<?php

/*
 * 2007-2012 PrestaShop
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
 * needs please refer to http://www.prestashop.com for more information.
 *
 *  @author PrestaShop SA <contact@prestashop.com>
 *  @copyright  2007-2012 PrestaShop SA
 *  @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 *  International Registered Trademark & Property of PrestaShop SA
 */

class CartCore extends ObjectModel {

    public $id;
    public $id_shop_group;
    public $id_shop;

    /** @var integer Customer delivery address ID */
    public $id_address_delivery;

    /** @var integer Customer invoicing address ID */
    public $id_address_invoice;

    /** @var integer Customer currency ID */
    public $id_currency;

    /** @var integer Customer ID */
    public $id_customer;

    /** @var integer Guest ID */
    public $id_guest;

    /** @var integer Language ID */
    public $id_lang;

    /** @var boolean True if the customer wants a recycled package */
    public $recyclable = 1;

    /** @var boolean True if the customer wants a gift wrapping */
    public $gift = 0;

    /** @var string Gift message if specified */
    public $gift_message;

    /** @var string Object creation date */
    public $date_add;

    /** @var string secure_key */
    public $secure_key;

    /** @var integer Carrier ID */
    public $id_carrier = 0;

    /** @var string Object last modification date */
    public $date_upd;
    public $checkedTos = false;
    public $pictures;
    public $textFields;
    public $delivery_option;

    /** @var boolean Allow to seperate order in multiple package in order to recieve as soon as possible the available products */
    public $allow_seperated_package = false;
    protected static $_nbProducts = array();
    protected static $_isVirtualCart = array();
    protected $_products = null;
    protected static $_totalWeight = array();
    protected $_taxCalculationMethod = PS_TAX_EXC;
    protected static $_carriers = null;
    protected static $_taxes_rate = null;
    protected static $_attributesLists = array();

    /**
     * @see ObjectModel::$definition
     */
    public static $definition = array(
        'table' => 'cart',
        'primary' => 'id_cart',
        'fields' => array(
            'id_shop_group' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'),
            'id_shop' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'),
            'id_address_delivery' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'),
            'id_address_invoice' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'),
            'id_carrier' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'),
            'id_currency' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true),
            'id_customer' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'),
            'id_guest' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId'),
            'id_lang' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true),
            'recyclable' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
            'gift' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
            'gift_message' => array('type' => self::TYPE_STRING, 'validate' => 'isMessage'),
            'delivery_option' => array('type' => self::TYPE_STRING),
            'secure_key' => array('type' => self::TYPE_STRING, 'size' => 32),
            'allow_seperated_package' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool'),
            'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDateFormat'),
            'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDateFormat'),
        ),
    );
    protected $webserviceParameters = array(
        'fields' => array(
            'id_address_delivery' => array('xlink_resource' => 'addresses'),
            'id_address_invoice' => array('xlink_resource' => 'addresses'),
            'id_currency' => array('xlink_resource' => 'currencies'),
            'id_customer' => array('xlink_resource' => 'customers'),
            'id_guest' => array('xlink_resource' => 'guests'),
            'id_lang' => array('xlink_resource' => 'languages'),
        ),
        'associations' => array(
            'cart_rows' => array('resource' => 'cart_row', 'virtual_entity' => true, 'fields' => array(
                    'id_product' => array('required' => true, 'xlink_resource' => 'products'),
                    'id_product_attribute' => array('required' => true, 'xlink_resource' => 'combinations'),
                    'quantity' => array('required' => true),
                )
            ),
        ),
    );

    const ONLY_PRODUCTS = 1;
    const ONLY_DISCOUNTS = 2;
    const BOTH = 3;
    const BOTH_WITHOUT_SHIPPING = 4;
    const ONLY_SHIPPING = 5;
    const ONLY_WRAPPING = 6;
    const ONLY_PRODUCTS_WITHOUT_SHIPPING = 7;
    const ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING = 8;

    public function __construct($id = null, $id_lang = null) {
        parent::__construct($id, $id_lang);
        if ($this->id_customer) {
            if (isset(Context::getContext()->customer) && Context::getContext()->customer->id == $this->id_customer)
                $customer = Context::getContext()->customer;
            else
                $customer = new Customer((int) $this->id_customer);

            if ((!$this->secure_key || $this->secure_key == '-1') && $customer->secure_key) {
                $this->secure_key = $customer->secure_key;
                $this->save();
            }
        }
        $this->_taxCalculationMethod = Group::getPriceDisplayMethod(Group::getCurrent()->id);
    }

    public function add($autodate = true, $null_values = false) {
        if (!$this->id_lang)
            $this->id_lang = Configuration::get('PS_LANG_DEFAULT');
        if (!$this->id_shop)
            $this->id_shop = Context::getContext()->shop->id;

        $return = parent::add($autodate);
        Hook::exec('actionCartSave');

        return $return;
    }

    public function update($null_values = false) {
        if (isset(self::$_nbProducts[$this->id]))
            unset(self::$_nbProducts[$this->id]);

        if (isset(self::$_totalWeight[$this->id]))
            unset(self::$_totalWeight[$this->id]);

        $this->_products = null;
        $return = parent::update();
        Hook::exec('actionCartSave');

        return $return;
    }

    /**
     * Update the address id of the cart
     * 
     * @param int $id_address Current address id to change
     * @param int $id_address_new New address id
     */
    public function updateAddressId($id_address, $id_address_new) {
        $to_update = false;
        if (!isset($this->id_address_invoice) || $this->id_address_invoice == $id_address) {
            $to_update = true;
            $this->context->cart->id_address_invoice = $id_address_new;
        }
        if (!isset($this->id_address_delivery) || $this->id_address_delivery == $id_address) {
            $to_update = true;
            $this->id_address_delivery = $id_address_new;
        }
        if ($to_update)
            $this->update();

        $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product`
		SET `id_address_delivery` = ' . (int) $id_address_new . '
		WHERE  `id_cart` = ' . (int) $this->id . '
			AND `id_address_delivery` = ' . (int) $id_address;
        Db::getInstance()->execute($sql);

        $sql = 'UPDATE `' . _DB_PREFIX_ . 'customization`
			SET `id_address_delivery` = ' . (int) $id_address_new . '
			WHERE  `id_cart` = ' . (int) $this->id . '
				AND `id_address_delivery` = ' . (int) $id_address;
        Db::getInstance()->execute($sql);
    }

    public function delete() {
        if ($this->OrderExists()) //NOT delete a cart which is associated with an order
            return false;

        $uploaded_files = Db::getInstance()->executeS('
			SELECT cd.`value`
			FROM `' . _DB_PREFIX_ . 'customized_data` cd
			INNER JOIN `' . _DB_PREFIX_ . 'customization` c ON (cd.`id_customization`= c.`id_customization`)
			WHERE cd.`type`= 0 AND c.`id_cart`=' . (int) $this->id
        );

        foreach ($uploaded_files as $must_unlink) {
            unlink(_PS_UPLOAD_DIR_ . $must_unlink['value'] . '_small');
            unlink(_PS_UPLOAD_DIR_ . $must_unlink['value']);
        }

        Db::getInstance()->execute('
			DELETE FROM `' . _DB_PREFIX_ . 'customized_data`
			WHERE `id_customization` IN (
				SELECT `id_customization`
				FROM `' . _DB_PREFIX_ . 'customization`
				WHERE `id_cart`=' . (int) $this->id . '
			)'
        );

        Db::getInstance()->execute('
			DELETE FROM `' . _DB_PREFIX_ . 'customization`
			WHERE `id_cart` = ' . (int) $this->id
        );

        if (!Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'cart_cart_rule` WHERE `id_cart` = ' . (int) $this->id)
                || !Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id))
            return false;

        return parent::delete();
    }

    public static function getTaxesAverageUsed($id_cart) {
        $cart = new Cart((int) $id_cart);
        if (!Validate::isLoadedObject($cart))
            die(Tools::displayError());

        if (!Configuration::get('PS_TAX'))
            return 0;

        $products = $cart->getProducts();
        $total_products_moy = 0;
        $ratio_tax = 0;

        if (!count($products))
            return 0;

        foreach ($products as $product) { // products refer to the cart details
            if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice')
                $address_id = (int) $cart->id_address_invoice;
            else
                $address_id = (int) $product->id_address_delivery; // Get delivery address of the product from the cart
            if (!Address::addressExists($address_id))
                $address_id = null;

            $total_products_moy += $product['total_wt'];
            $ratio_tax += $product['total_wt'] * Tax::getProductTaxRate(
                            (int) $product['id_product'], (int) $address_id
            );
        }

        if ($total_products_moy > 0)
            return $ratio_tax / $total_products_moy;

        return 0;
    }

    /**
     * @deprecated 1.5.0, use Cart->getCartRules()
     */
    public function getDiscounts($lite = false, $refresh = false) {
        Tools::displayAsDeprecated();
        return $this->getCartRules();
    }

    public function getCartRules($filter = CartRule::FILTER_ACTION_ALL) {
        // If the cart has not been saved, then there can't be any cart rule applied
        if (!CartRule::isFeatureActive() || !$this->id)
            return array();

        $cache_key = 'Cart::getCartRules' . $this->id . '-' . $filter;
        if (!Cache::isStored($cache_key)) {
            $result = Db::getInstance()->executeS('
				SELECT *
				FROM `' . _DB_PREFIX_ . 'cart_cart_rule` cd
				LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule` cr ON cd.`id_cart_rule` = cr.`id_cart_rule`
				LEFT JOIN `' . _DB_PREFIX_ . 'cart_rule_lang` crl ON (
					cd.`id_cart_rule` = crl.`id_cart_rule`
					AND crl.id_lang = ' . (int) $this->id_lang . '
				)
				WHERE `id_cart` = ' . (int) $this->id . '
				' . ($filter == CartRule::FILTER_ACTION_SHIPPING ? 'AND free_shipping = 1' : '') . '
				' . ($filter == CartRule::FILTER_ACTION_GIFT ? 'AND gift_product != 0' : '') . '
				' . ($filter == CartRule::FILTER_ACTION_REDUCTION ? 'AND (reduction_percent != 0 OR reduction_amount != 0)' : '')
            );
            Cache::store($cache_key, $result);
        }
        $result = Cache::retrieve($cache_key);

        // Define virtual context to prevent case where the cart is not the in the global context
        $virtual_context = Context::getContext()->cloneContext();
        $virtual_context->cart = $this;

        foreach ($result as &$row) {
            $row['obj'] = new CartRule($row['id_cart_rule'], (int) $this->id_lang);
            $row['value_real'] = $row['obj']->getContextualValue(true, $virtual_context, $filter);
            $row['value_tax_exc'] = $row['obj']->getContextualValue(false, $virtual_context, $filter);

            // Retro compatibility < 1.5.0.2
            $row['id_discount'] = $row['id_cart_rule'];
            $row['description'] = $row['name'];
        }

        return $result;
    }

    public function getDiscountsCustomer($id_cart_rule) {
        if (!CartRule::isFeatureActive())
            return 0;

        return Db::getInstance()->getValue('
			SELECT COUNT(*)
			FROM `' . _DB_PREFIX_ . 'cart_cart_rule`
			WHERE `id_cart_rule` = ' . (int) $id_cart_rule . ' AND `id_cart` = ' . (int) $this->id
        );
    }

    public function getLastProduct() {
        $sql = '
			SELECT `id_product`, `id_product_attribute`, id_shop
			FROM `' . _DB_PREFIX_ . 'cart_product`
			WHERE `id_cart` = ' . (int) $this->id . '
			ORDER BY `date_add` DESC';

        $result = Db::getInstance()->getRow($sql);
        if ($result && isset($result['id_product']) && $result['id_product'])
            foreach ($this->getProducts() as $product)
                if ($result['id_product'] == $product['id_product']
                        && (
                        !$result['id_product_attribute']
                        || $result['id_product_attribute'] == $product['id_product_attribute']
                        ))
                    return $product;

        return false;
    }

    /**
     * Return cart products
     *
     * @result array Products
     */
    public function getProducts($refresh = false, $id_product = false, $id_country = null) {
        if (!$this->id)
            return array();
        // Product cache must be strictly compared to NULL, or else an empty cart will add dozens of queries
        if ($this->_products !== null && !$refresh) {
            // Return product row with specified ID if it exists
            if (is_int($id_product)) {
                foreach ($this->_products as $product)
                    if ($product['id_product'] == $id_product)
                        return array($product);
                return array();
            }
            return $this->_products;
        }

        // Build query
        $sql = new DbQuery();

        // Build SELECT
        $sql->select('cp.`id_product_attribute`, cp.`id_product`, cp.`quantity` AS cart_quantity, cp.id_shop, pl.`name`, p.`is_virtual`,
						pl.`description_short`, pl.`available_now`, pl.`available_later`, p.`id_product`, product_shop.`id_category_default`, p.`id_supplier`,
						p.`id_manufacturer`, product_shop.`on_sale`, product_shop.`ecotax`, product_shop.`additional_shipping_cost`, product_shop.`available_for_order`, product_shop.`price`, p.`weight`,
						stock.`quantity` quantity_available, p.`width`, p.`height`, p.`depth`, stock.`out_of_stock`, product_shop.`active`, p.`date_add`,
						p.`date_upd`, IFNULL(stock.quantity, 0) as quantity, pl.`link_rewrite`, cl.`link_rewrite` AS category,
						CONCAT(cp.`id_product`, cp.`id_product_attribute`, cp.`id_address_delivery`) AS unique_id, cp.id_address_delivery,
						product_shop.`wholesale_price`, product_shop.advanced_stock_management');

        // Build FROM
        $sql->from('cart_product', 'cp');

        // Build JOIN
        $sql->leftJoin('product', 'p', 'p.`id_product` = cp.`id_product`');
        $sql->innerJoin('product_shop', 'product_shop', '(product_shop.id_shop=cp.id_shop AND product_shop.id_product = p.id_product)');
        $sql->leftJoin('product_lang', 'pl', '
			p.`id_product` = pl.`id_product`
			AND pl.`id_lang` = ' . (int) $this->id_lang . Shop::addSqlRestrictionOnLang('pl', 'cp.id_shop')
        );


        $sql->leftJoin('category_lang', 'cl', '
			product_shop.`id_category_default` = cl.`id_category`
			AND cl.`id_lang` = ' . (int) $this->id_lang . Shop::addSqlRestrictionOnLang('cl', 'cp.id_shop')
        );

        // @todo test if everything is ok, then refactorise call of this method
        $sql->join(Product::sqlStock('cp', 'cp'));

        // Build WHERE clauses
        $sql->where('cp.`id_cart` = ' . (int) $this->id);
        if ($id_product)
            $sql->where('cp.`id_product` = ' . (int) $id_product);
        $sql->where('p.`id_product` IS NOT NULL');

        // Build GROUP BY
        $sql->groupBy('unique_id');

        // Build ORDER BY
        $sql->orderBy('p.id_product, cp.id_product_attribute, cp.date_add ASC');

        if (Customization::isFeatureActive()) {
            $sql->select('cu.`id_customization`, cu.`quantity` AS customization_quantity');
            $sql->leftJoin('customization', 'cu', 'p.`id_product` = cu.`id_product` AND cp.`id_product_attribute` = cu.id_product_attribute AND cu.id_cart=' . (int) $this->id);
        }
        else
            $sql->select('NULL AS customization_quantity, NULL AS id_customization');

        if (Combination::isFeatureActive()) {
            $sql->select('
				product_attribute_shop.`price` AS price_attribute, product_attribute_shop.`ecotax` AS ecotax_attr,
				IF (IFNULL(pa.`reference`, \'\') = \'\', p.`reference`, pa.`reference`) AS reference,
				IF (IFNULL(pa.`supplier_reference`, \'\') = \'\', p.`supplier_reference`, pa.`supplier_reference`) AS supplier_reference,
				(p.`weight`+ pa.`weight`) weight_attribute,
				IF (IFNULL(pa.`ean13`, \'\') = \'\', p.`ean13`, pa.`ean13`) AS ean13,
				IF (IFNULL(pa.`upc`, \'\') = \'\', p.`upc`, pa.`upc`) AS upc,
				pai.`id_image` as pai_id_image, il.`legend` as pai_legend,
				IFNULL(product_attribute_shop.`minimal_quantity`, product_shop.`minimal_quantity`) as minimal_quantity
			');

            $sql->leftJoin('product_attribute', 'pa', 'pa.`id_product_attribute` = cp.`id_product_attribute`');
            $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', '(product_attribute_shop.id_shop=cp.id_shop AND product_attribute_shop.id_product_attribute = pa.id_product_attribute)');
            $sql->leftJoin('product_attribute_image', 'pai', 'pai.`id_product_attribute` = pa.`id_product_attribute`');
            $sql->leftJoin('image_lang', 'il', 'il.id_image = pai.id_image AND il.id_lang = ' . (int) $this->id_lang);
        }
        else
            $sql->select(
                    'p.`reference` AS reference, p.`supplier_reference` AS supplier_reference, p.`ean13`,
				p.`upc` AS upc, product_shop.`minimal_quantity` AS minimal_quantity'
            );
        $result = Db::getInstance()->executeS($sql);

        // Reset the cache before the following return, or else an empty cart will add dozens of queries
        $products_ids = array();
        $pa_ids = array();
        if ($result)
            foreach ($result as $row) {
                $products_ids[] = $row['id_product'];
                $pa_ids[] = $row['id_product_attribute'];
            }
        // Thus you can avoid one query per product, because there will be only one query for all the products of the cart
        Product::cacheProductsFeatures($products_ids);
        Cart::cacheSomeAttributesLists($pa_ids, $this->id_lang);

        $this->_products = array();
        if (empty($result))
            return array();

        $cart_shop_context = Context::getContext()->cloneContext();
        foreach ($result as &$row) {
            if (isset($row['ecotax_attr']) && $row['ecotax_attr'] > 0)
                $row['ecotax'] = (float) $row['ecotax_attr'];

            $row['stock_quantity'] = (int) $row['quantity'];
            // for compatibility with 1.2 themes
            $row['quantity'] = (int) $row['cart_quantity'];

            if (isset($row['id_product_attribute']) && (int) $row['id_product_attribute'] && isset($row['weight_attribute']))
                $row['weight'] = (float) $row['weight_attribute'];

            if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice')
                $address_id = (int) $this->id_address_invoice;
            else
                $address_id = (int) $row['id_address_delivery'];
            if (!Address::addressExists($address_id))
                $address_id = null;

            if ($cart_shop_context->shop->id != $row['id_shop'])
                $cart_shop_context->shop = new Shop((int) $row['id_shop']);

            if ($this->_taxCalculationMethod == PS_TAX_EXC) {
                $row['price'] = Product::getPriceStatic(
                                (int) $row['id_product'], false, isset($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null, 2, null, false, true, (int) $row['cart_quantity'], false, ((int) $this->id_customer ? (int) $this->id_customer : null), (int) $this->id, ((int) $address_id ? (int) $address_id : null), $specific_price_output, true, true, $cart_shop_context
                ); // Here taxes are computed only once the quantity has been applied to the product price
                
                $row['price_wt'] = Product::getPriceStatic(
                                (int) $row['id_product'], true, isset($row['id_product_attribute']) ? (int) $row['id_product_attribute'] : null, 2, null, false, true, (int) $row['cart_quantity'], false, ((int) $this->id_customer ? (int) $this->id_customer : null), (int) $this->id, ((int) $address_id ? (int) $address_id : null), $null, true, true, $cart_shop_context
                );

                $tax_rate = Tax::getProductTaxRate((int) $row['id_product'], (int) $address_id);

                $row['total_wt'] = Tools::ps_round($row['price'] * (float) $row['cart_quantity'] * (1 + (float) $tax_rate / 100), 2);
                $row['total'] = $row['price'] * (int) $row['cart_quantity'];
            } else {
                $row['price'] = Product::getPriceStatic(
                                (int) $row['id_product'], false, (int) $row['id_product_attribute'], 2, null, false, true, $row['cart_quantity'], false, ((int) $this->id_customer ? (int) $this->id_customer : null), (int) $this->id, ((int) $address_id ? (int) $address_id : null), $specific_price_output, true, true, $cart_shop_context
                );
                
                $row['price_wt'] = Product::getPriceStatic(
                                (int) $row['id_product'], true, (int) $row['id_product_attribute'], 2, null, false, true, $row['cart_quantity'], false, ((int) $this->id_customer ? (int) $this->id_customer : null), (int) $this->id, ((int) $address_id ? (int) $address_id : null), $null, true, true, $cart_shop_context
                );

                // In case when you use QuantityDiscount, getPriceStatic() can be return more of 2 decimals
                $row['price_wt'] = Tools::ps_round($row['price_wt'], 2);
                $row['total_wt'] = $row['price_wt'] * (int) $row['cart_quantity'];
                $row['total'] = Tools::ps_round($row['price'] * (int) $row['cart_quantity'], 2);
            }

            if (!isset($row['pai_id_image']) || $row['pai_id_image'] == 0) {
                $row2 = Db::getInstance()->getRow('
					SELECT image_shop.`id_image` id_image, il.`legend`
					FROM `' . _DB_PREFIX_ . 'image` i' .
                        Shop::addSqlAssociation('image', 'i', false, 'image_shop.cover=1') . '
					LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il ON (image_shop.`id_image` = il.`id_image` AND il.`id_lang` = ' . (int) $this->id_lang . ')
					WHERE i.`id_product` = ' . (int) $row['id_product'] . ' AND image_shop.`cover` = 1'
                );

                if (!$row2)
                    $row2 = array('id_image' => false, 'legend' => false);
                else
                    $row = array_merge($row, $row2);
            }
            else {
                $row['id_image'] = $row['pai_id_image'];
                $row['legend'] = $row['pai_legend'];
            }

            $row['reduction_applies'] = ($specific_price_output && (float) $specific_price_output['reduction']);
            $row['quantity_discount_applies'] = ($specific_price_output && $row['cart_quantity'] >= (int) $specific_price_output['from_quantity']);
            $row['id_image'] = Product::defineProductImage($row, $this->id_lang);
            $row['allow_oosp'] = Product::isAvailableWhenOutOfStock($row['out_of_stock']);
            $row['features'] = Product::getFeaturesStatic((int) $row['id_product']);

            if (array_key_exists($row['id_product_attribute'] . '-' . $this->id_lang, self::$_attributesLists))
                $row = array_merge($row, self::$_attributesLists[$row['id_product_attribute'] . '-' . $this->id_lang]);

            $row = Product::getTaxesInformations($row, $cart_shop_context);

            $this->_products[] = $row;
        }
//        //tuannd added 05/04/2013 : change price 
//        $BufferProducts = array();
//        foreach ($this->_products as $item) {
//            $item['price_wt'] = Context::getContext()->cookie->__get($item['id_product']."_".$item['id_product_attribute']);
//            $item['price'] = Context::getContext()->cookie->__get($item['id_product']."_".$item['id_product_attribute']);
//            $BufferProducts[] = $item;
//        }
//        $this->_products = $BufferProducts;
        //tuannd end 05/04/2013
        return $this->_products;
    }

    public static function cacheSomeAttributesLists($ipa_list, $id_lang) {
        if (!Combination::isFeatureActive())
            return;

        $pa_implode = array();

        foreach ($ipa_list as $id_product_attribute)
            if ((int) $id_product_attribute && !array_key_exists($id_product_attribute . '-' . $id_lang, self::$_attributesLists)) {
                $pa_implode[] = (int) $id_product_attribute;
                self::$_attributesLists[(int) $id_product_attribute . '-' . $id_lang] = array('attributes' => '', 'attributes_small' => '');
            }

        if (!count($pa_implode))
            return;

        $result = Db::getInstance()->executeS('
			SELECT pac.`id_product_attribute`, agl.`public_name` AS public_group_name, al.`name` AS attribute_name
			FROM `' . _DB_PREFIX_ . 'product_attribute_combination` pac
			LEFT JOIN `' . _DB_PREFIX_ . 'attribute` a ON a.`id_attribute` = pac.`id_attribute`
			LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group` ag ON ag.`id_attribute_group` = a.`id_attribute_group`
			LEFT JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ON (
				a.`id_attribute` = al.`id_attribute`
				AND al.`id_lang` = ' . (int) $id_lang . '
			)
			LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ON (
				ag.`id_attribute_group` = agl.`id_attribute_group`
				AND agl.`id_lang` = ' . (int) $id_lang . '
			)
			WHERE pac.`id_product_attribute` IN (' . implode($pa_implode, ',') . ')
			ORDER BY agl.`public_name` ASC'
        );

        foreach ($result as $row) {
            self::$_attributesLists[$row['id_product_attribute'] . '-' . $id_lang]['attributes'] .= $row['public_group_name'] . ' : ' . $row['attribute_name'] . ', ';
            self::$_attributesLists[$row['id_product_attribute'] . '-' . $id_lang]['attributes_small'] .= $row['attribute_name'] . ', ';
        }

        foreach ($pa_implode as $id_product_attribute) {
            self::$_attributesLists[$id_product_attribute . '-' . $id_lang]['attributes'] = rtrim(
                    self::$_attributesLists[$id_product_attribute . '-' . $id_lang]['attributes'], ', '
            );

            self::$_attributesLists[$id_product_attribute . '-' . $id_lang]['attributes_small'] = rtrim(
                    self::$_attributesLists[$id_product_attribute . '-' . $id_lang]['attributes_small'], ', '
            );
        }
    }

    /**
     * Return cart products quantity
     *
     * @result integer Products quantity
     */
    public function nbProducts() {
        if (!$this->id)
            return 0;

        return Cart::getNbProducts($this->id);
    }

    public static function getNbProducts($id) {
        // Must be strictly compared to NULL, or else an empty cart will bypass the cache and add dozens of queries
        if (isset(self::$_nbProducts[$id]) && self::$_nbProducts[$id] !== null)
            return self::$_nbProducts[$id];

        self::$_nbProducts[$id] = (int) Db::getInstance()->getValue('
			SELECT SUM(`quantity`)
			FROM `' . _DB_PREFIX_ . 'cart_product`
			WHERE `id_cart` = ' . (int) $id
        );

        return self::$_nbProducts[$id];
    }

    /**
     * @deprecated 1.5.0, use Cart->addCartRule()
     */
    public function addDiscount($id_cart_rule) {
        Tools::displayAsDeprecated();
        return $this->addCartRule($id_cart_rule);
    }

    public function addCartRule($id_cart_rule) {
        // You can't add a cart rule that does not exist
        $cartRule = new CartRule($id_cart_rule, Context::getContext()->language->id);
        if (!Validate::isLoadedObject($cartRule))
            return false;

        // Add the cart rule to the cart
        if (!Db::getInstance()->insert('cart_cart_rule', array(
                    'id_cart_rule' => (int) $id_cart_rule,
                    'id_cart' => (int) $this->id
                )))
            return false;

        Cache::clean('Cart::getCartRules' . $this->id . '-' . CartRule::FILTER_ACTION_ALL);
        Cache::clean('Cart::getCartRules' . $this->id . '-' . CartRule::FILTER_ACTION_SHIPPING);
        Cache::clean('Cart::getCartRules' . $this->id . '-' . CartRule::FILTER_ACTION_REDUCTION);
        Cache::clean('Cart::getCartRules' . $this->id . '-' . CartRule::FILTER_ACTION_GIFT);

        if ((int) $cartRule->gift_product)
            $this->updateQty(1, $cartRule->gift_product, $cartRule->gift_product_attribute, false, 'up', 0, null, false);

        return true;
    }

    public function containsProduct($id_product, $id_product_attribute = 0, $id_customization = false, $id_address_delivery = 0) {
        $sql = 'SELECT cp.`quantity` FROM `' . _DB_PREFIX_ . 'cart_product` cp';

        if ($id_customization)
            $sql .= '
				LEFT JOIN `' . _DB_PREFIX_ . 'customization` c ON (
					c.`id_product` = cp.`id_product`
					AND c.`id_product_attribute` = cp.`id_product_attribute`
				)';

        $sql .= '
			WHERE cp.`id_product` = ' . (int) $id_product . '
			AND cp.`id_product_attribute` = ' . (int) $id_product_attribute . '
			AND cp.`id_cart` = ' . (int) $this->id;
        if (Configuration::get('PS_ALLOW_MULTISHIPPING') && $this->isMultiAddressDelivery())
            $sql .= ' AND cp.`id_address_delivery` = ' . (int) $id_address_delivery;

        if ($id_customization)
            $sql .= ' AND c.`id_customization` = ' . (int) $id_customization;

        return Db::getInstance()->getRow($sql);
    }

    /**
     * Update product quantity
     *
     * @param integer $quantity Quantity to add (or substract)
     * @param integer $id_product Product ID
     * @param integer $id_product_attribute Attribute ID if needed
     * @param string $operator Indicate if quantity must be increased or decreased
     */
    public function updateQty($quantity, $id_product, $id_product_attribute = null, $id_customization = false, $operator = 'up', $id_address_delivery = 0, Shop $shop = null, $auto_add_cart_rule = true, $finalPrice = 0.0) {
        if (!$shop)
            $shop = Context::getContext()->shop;

        if (Context::getContext()->customer->id) {
            if ($id_address_delivery == 0 && (int) $this->id_address_delivery) // The $id_address_delivery is null, use the cart delivery address
                $id_address_delivery = $this->id_address_delivery;
            elseif ($id_address_delivery == 0) // The $id_address_delivery is null, get the default customer address
                $id_address_delivery = (int) Address::getFirstCustomerAddressId((int) Context::getContext()->customer->id);
            elseif (!Customer::customerHasAddress(Context::getContext()->customer->id, $id_address_delivery)) // The $id_address_delivery must be linked with customer
                $id_address_delivery = 0;
        }

        $quantity = (int) $quantity;
        $id_product = (int) $id_product;
        $id_product_attribute = (int) $id_product_attribute;
        $product = new Product($id_product, false, Configuration::get('PS_LANG_DEFAULT'), $shop->id);

        if ($id_product_attribute) {
            $combination = new Combination((int) $id_product_attribute);
            if ($combination->id_product != $id_product)
                return false;
        }

        /* If we have a product combination, the minimal quantity is set with the one of this combination */
        if (!empty($id_product_attribute))
            $minimal_quantity = (int) Attribute::getAttributeMinimalQty($id_product_attribute);
        else
            $minimal_quantity = (int) $product->minimal_quantity;

        if (!Validate::isLoadedObject($product))
            die(Tools::displayError());

        if (isset(self::$_nbProducts[$this->id]))
            unset(self::$_nbProducts[$this->id]);

        if (isset(self::$_totalWeight[$this->id]))
            unset(self::$_totalWeight[$this->id]);

        if ((int) $quantity <= 0)
            return $this->deleteProduct($id_product, $id_product_attribute, (int) $id_customization);
        elseif (!$product->available_for_order || Configuration::get('PS_CATALOG_MODE'))
            return false;
        else {
            /* Check if the product is already in the cart */
            $result = $this->containsProduct($id_product, $id_product_attribute, (int) $id_customization, (int) $id_address_delivery);

            /* Update quantity if product already exist */
            if ($result) {
                if ($operator == 'up') {
                    $sql = 'SELECT stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity
							FROM ' . _DB_PREFIX_ . 'product p
							' . Product::sqlStock('p', $id_product_attribute, true, $shop) . '
							WHERE p.id_product = ' . $id_product;

                    $result2 = Db::getInstance()->getRow($sql);
                    $product_qty = (int) $result2['quantity'];
                    // Quantity for product pack
                    if (Pack::isPack($id_product))
                        $product_qty = Pack::getQuantity($id_product, $id_product_attribute);
                    $new_qty = (int) $result['quantity'] + (int) $quantity;
                    $qty = '+ ' . (int) $quantity;

                    if (!Product::isAvailableWhenOutOfStock((int) $result2['out_of_stock']))
                        if ($new_qty > $product_qty)
                            return false;
                }
                else if ($operator == 'down') {
                    $qty = '- ' . (int) $quantity;
                    $new_qty = (int) $result['quantity'] - (int) $quantity;
                    if ($new_qty < $minimal_quantity && $minimal_quantity > 1)
                        return -1;
                }
                else
                    return false;

                /* Delete product from cart */
                if ($new_qty <= 0)
                    return $this->deleteProduct((int) $id_product, (int) $id_product_attribute, (int) $id_customization);
                else if ($new_qty < $minimal_quantity)
                    return -1;
                else
                    Db::getInstance()->execute('
						UPDATE `' . _DB_PREFIX_ . 'cart_product`
						SET `quantity` = `quantity` ' . $qty . ', `date_add` = NOW()
						WHERE `id_product` = ' . (int) $id_product .
                            (!empty($id_product_attribute) ? ' AND `id_product_attribute` = ' . (int) $id_product_attribute : '') . '
						AND `id_cart` = ' . (int) $this->id . (Configuration::get('PS_ALLOW_MULTISHIPPING') && $this->isMultiAddressDelivery() ? ' AND `id_address_delivery` = ' . (int) $id_address_delivery : '') . '
						LIMIT 1'
                    );
            }
            /* Add product to the cart */
            elseif ($operator == 'up') {
                $sql = 'SELECT stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity
						FROM ' . _DB_PREFIX_ . 'product p
						' . Product::sqlStock('p', $id_product_attribute, true, $shop) . '
						WHERE p.id_product = ' . $id_product;

                $result2 = Db::getInstance()->getRow($sql);

                // Quantity for product pack
                if (Pack::isPack($id_product))
                    $result2['quantity'] = Pack::getQuantity($id_product, $id_product_attribute);

                if (!Product::isAvailableWhenOutOfStock((int) $result2['out_of_stock']))
                    if ((int) $quantity > $result2['quantity'])
                        return false;

                if ((int) $quantity < $minimal_quantity)
                    return -1;
                //hung.nguyentuan 4/10/2013 add finalPrice value to ps_cart_product table
                $result_add = Db::getInstance()->insert('cart_product', array(
                    'id_product' => (int) $id_product,
                    'id_product_attribute' => (int) $id_product_attribute,
                    'id_cart' => (int) $this->id,
                    'id_address_delivery' => (int) $id_address_delivery,
                    'id_shop' => $shop->id,
                    'quantity' => (int) $quantity,
                    'date_add' => date('Y-m-d H:i:s'),
                    'finalPrice' => (float) $finalPrice
                        ));

                if (!$result_add)
                    return false;
            }
        }

        // refresh cache of self::_products
        $this->_products = $this->getProducts(true);
        $this->update(true);
        $context = Context::getContext()->cloneContext();
        $context->cart = $this;
        if ($auto_add_cart_rule)
            CartRule::autoAddToCart($context);

        if ($product->customizable)
            return $this->_updateCustomizationQuantity((int) $quantity, (int) $id_customization, (int) $id_product, (int) $id_product_attribute, (int) $id_address_delivery, $operator);
        else
            return true;
    }

    /*
     * * Customization management
     */

    protected function _updateCustomizationQuantity($quantity, $id_customization, $id_product, $id_product_attribute, $id_address_delivery, $operator = 'up') {
        // Link customization to product combination when it is first added to cart
        if (empty($id_customization)) {
            $customization = $this->getProductCustomization($id_product, null, true);
            foreach ($customization as $field) {
                if ($field['quantity'] == 0) {
                    Db::getInstance()->execute('
					UPDATE `' . _DB_PREFIX_ . 'customization`
					SET `quantity` = ' . (int) $quantity . ',
						`id_product_attribute` = ' . (int) $id_product_attribute . ',
						`id_address_delivery` = ' . (int) $id_address_delivery . ',
						`in_cart` = 1
					WHERE `id_customization` = ' . (int) $field['id_customization']);
                }
            }
        }

        /* Deletion */
        if (!empty($id_customization) && (int) $quantity < 1)
            return $this->_deleteCustomization((int) $id_customization, (int) $id_product, (int) $id_product_attribute);

        /* Quantity update */
        if (!empty($id_customization)) {
            $result = Db::getInstance()->getRow('SELECT `quantity` FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_customization` = ' . (int) $id_customization);
            if ($result && Db::getInstance()->NumRows()) {
                if ($operator == 'down' && (int) $result['quantity'] - (int) $quantity < 1)
                    return Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'customization` WHERE `id_customization` = ' . (int) $id_customization);

                return Db::getInstance()->execute('
					UPDATE `' . _DB_PREFIX_ . 'customization`
					SET
						`quantity` = `quantity` ' . ($operator == 'up' ? '+ ' : '- ') . (int) $quantity . ',
						`id_address_delivery` = ' . (int) $id_address_delivery . '
					WHERE `id_customization` = ' . (int) $id_customization);
            }
            else
                Db::getInstance()->execute('
					UPDATE `' . _DB_PREFIX_ . 'customization`
					SET `id_address_delivery` = ' . (int) $id_address_delivery . '
					WHERE `id_customization` = ' . (int) $id_customization);
        }
        // refresh cache of self::_productgetProductss
        $this->_products = $this->getProducts(true);
        $this->update(true);
        return true;
    }

    /**
     * Add customization item to database
     *
     * @param int $id_product
     * @param int $id_product_attribute
     * @param int $index
     * @param int $type
     * @param string $field
     * @param int $quantity
     * @return boolean success
     */
    public function _addCustomization($id_product, $id_product_attribute, $index, $type, $field, $quantity) {
        $exising_customization = Db::getInstance()->executeS('
			SELECT cu.`id_customization`, cd.`index`, cd.`value`, cd.`type` FROM `' . _DB_PREFIX_ . 'customization` cu
			LEFT JOIN `' . _DB_PREFIX_ . 'customized_data` cd
			ON cu.`id_customization` = cd.`id_customization`
			WHERE cu.id_cart = ' . (int) $this->id . '
			AND cu.id_product = ' . (int) $id_product . '
			AND in_cart = 0'
        );

        if ($exising_customization) {
            // If the customization field is alreay filled, delete it
            foreach ($exising_customization as $customization) {
                if ($customization['type'] == $type && $customization['index'] == $index) {
                    Db::getInstance()->execute('
						DELETE FROM `' . _DB_PREFIX_ . 'customized_data`
						WHERE id_customization = ' . (int) $customization['id_customization'] . '
						AND type = ' . (int) $customization['type'] . '
						AND `index` = ' . (int) $customization['index']);
                    if ($type == Product::CUSTOMIZE_FILE) {
                        @unlink(_PS_UPLOAD_DIR_ . $customization['value']);
                        @unlink(_PS_UPLOAD_DIR_ . $customization['value'] . '_small');
                    }
                    break;
                }
            }
            $id_customization = $exising_customization[0]['id_customization'];
        } else {
            Db::getInstance()->execute(
                    'INSERT INTO `' . _DB_PREFIX_ . 'customization` (`id_cart`, `id_product`, `id_product_attribute`, `quantity`)
				VALUES (' . (int) $this->id . ', ' . (int) $id_product . ', ' . (int) $id_product_attribute . ', ' . (int) $quantity . ')'
            );
            $id_customization = Db::getInstance()->Insert_ID();
        }

        $query = 'INSERT INTO `' . _DB_PREFIX_ . 'customized_data` (`id_customization`, `type`, `index`, `value`)
			VALUES (' . (int) $id_customization . ', ' . (int) $type . ', ' . (int) $index . ', \'' . pSql($field) . '\')';

        if (!Db::getInstance()->execute($query))
            return false;
        return true;
    }

    /**
     * Check if order has already been placed
     *
     * @return boolean result
     */
    public function orderExists() {
        return (bool) Db::getInstance()->getValue('SELECT count(*) FROM `' . _DB_PREFIX_ . 'orders` WHERE `id_cart` = ' . (int) $this->id);
    }

    /**
     * @deprecated 1.5.0, use Cart->removeCartRule()
     */
    public function deleteDiscount($id_cart_rule) {
        Tools::displayAsDeprecated();
        return $this->removeCartRule($id_cart_rule);
    }

    public function removeCartRule($id_cart_rule) {
        Cache::clean('Cart::getCartRules' . $this->id . '-' . CartRule::FILTER_ACTION_ALL);
        Cache::clean('Cart::getCartRules' . $this->id . '-' . CartRule::FILTER_ACTION_SHIPPING);
        Cache::clean('Cart::getCartRules' . $this->id . '-' . CartRule::FILTER_ACTION_REDUCTION);
        Cache::clean('Cart::getCartRules' . $this->id . '-' . CartRule::FILTER_ACTION_GIFT);

        $result = Db::getInstance()->execute('
		DELETE FROM `' . _DB_PREFIX_ . 'cart_cart_rule`
		WHERE `id_cart_rule` = ' . (int) $id_cart_rule . '
		AND `id_cart` = ' . (int) $this->id . '
		LIMIT 1');

        $cart_rule = new CartRule($id_cart_rule, Configuration::get('PS_LANG_DEFAULT'));
        if ((int) $cart_rule->gift_product)
            $this->updateQty(1, $cart_rule->gift_product, $cart_rule->gift_product_attribute, null, 'down', 0, null, false);

        return $result;
    }

    /**
     * Delete a product from the cart
     *
     * @param integer $id_product Product ID
     * @param integer $id_product_attribute Attribute ID if needed
     * @param integer $id_customization Customization id
     * @return boolean result
     */
    public function deleteProduct($id_product, $id_product_attribute = null, $id_customization = null, $id_address_delivery = 0) {
        if (isset(self::$_nbProducts[$this->id]))
            unset(self::$_nbProducts[$this->id]);

        if (isset(self::$_totalWeight[$this->id]))
            unset(self::$_totalWeight[$this->id]);

        if ((int) $id_customization) {
            $product_total_quantity = (int) Db::getInstance()->getValue(
                            'SELECT `quantity`
				FROM `' . _DB_PREFIX_ . 'cart_product`
				WHERE `id_product` = ' . (int) $id_product . '
				AND `id_product_attribute` = ' . (int) $id_product_attribute
            );

            $customization_quantity = (int) Db::getInstance()->getValue('
			SELECT `quantity`
			FROM `' . _DB_PREFIX_ . 'customization`
			WHERE `id_cart` = ' . (int) $this->id . '
			AND `id_product` = ' . (int) $id_product . '
			AND `id_product_attribute` = ' . (int) $id_product_attribute . '
			' . ((int) $id_address_delivery ? 'AND `id_address_delivery` = ' . (int) $id_address_delivery : ''));

            if (!$this->_deleteCustomization((int) $id_customization, (int) $id_product, (int) $id_product_attribute, $id_address_delivery))
                return false;

            // refresh cache of self::_products
            $this->_products = $this->getProducts(true);
            return ($this->deleteProduct((int) $id_product, $id_product_attribute, $id_address_delivery) && $customization_quantity == $product_total_quantity);
        }

        /* Get customization quantity */
        $result = Db::getInstance()->getRow('
			SELECT SUM(`quantity`) AS \'quantity\'
			FROM `' . _DB_PREFIX_ . 'customization`
			WHERE `id_cart` = ' . (int) $this->id . '
			AND `id_product` = ' . (int) $id_product . '
			AND `id_product_attribute` = ' . (int) $id_product_attribute
        );

        if ($result === false)
            return false;

        /* If the product still possesses customization it does not have to be deleted */
        if (Db::getInstance()->NumRows() && (int) $result['quantity'])
            return Db::getInstance()->execute('
				UPDATE `' . _DB_PREFIX_ . 'cart_product`
				SET `quantity` = ' . (int) $result['quantity'] . '
				WHERE `id_cart` = ' . (int) $this->id . '
				AND `id_product` = ' . (int) $id_product .
                            ($id_product_attribute != null ? ' AND `id_product_attribute` = ' . (int) $id_product_attribute : '')
            );

        /* Product deletion */
        $result = Db::getInstance()->execute('
		DELETE FROM `' . _DB_PREFIX_ . 'cart_product`
		WHERE `id_product` = ' . (int) $id_product . '
		' . (!is_null($id_product_attribute) ? ' AND `id_product_attribute` = ' . (int) $id_product_attribute : '') . '
		AND `id_cart` = ' . (int) $this->id . '
		' . ((int) $id_address_delivery ? 'AND `id_address_delivery` = ' . (int) $id_address_delivery : ''));

        if ($result) {
            $return = $this->update(true);
            // refresh cache of self::_products
            $this->_products = $this->getProducts(true);
            CartRule::autoRemoveFromCart();

            return $return;
        }

        return false;
    }

    /**
     * Delete a customization from the cart. If customization is a Picture,
     * then the image is also deleted
     *
     * @param integer $id_customization
     * @return boolean result
     */
    protected function _deleteCustomization($id_customization, $id_product, $id_product_attribute, $id_address_delivery = 0) {
        $result = true;
        $customization = Db::getInstance()->getRow('SELECT *
			FROM `' . _DB_PREFIX_ . 'customization`
			WHERE `id_customization` = ' . (int) $id_customization);

        if ($customization) {
            $cust_data = Db::getInstance()->getRow('SELECT *
				FROM `' . _DB_PREFIX_ . 'customized_data`
				WHERE `id_customization` = ' . (int) $id_customization);

            // Delete customization picture if necessary
            if (isset($cust_data['type']) && $cust_data['type'] == 0)
                $result &= (@unlink(_PS_UPLOAD_DIR_ . $cust_data['value']) && @unlink(_PS_UPLOAD_DIR_ . $cust_data['value'] . '_small'));

            $result &= Db::getInstance()->execute(
                    'DELETE FROM `' . _DB_PREFIX_ . 'customized_data`
				WHERE `id_customization` = ' . (int) $id_customization
            );

            if ($result)
                $result &= Db::getInstance()->execute(
                        'UPDATE `' . _DB_PREFIX_ . 'cart_product`
					SET `quantity` = `quantity` - ' . (int) $customization['quantity'] . '
					WHERE `id_cart` = ' . (int) $this->id . '
					AND `id_product` = ' . (int) $id_product .
                        ((int) $id_product_attribute ? ' AND `id_product_attribute` = ' . (int) $id_product_attribute : '') . '
					AND `id_address_delivery` = ' . (int) $id_address_delivery
                );

            if (!$result)
                return false;

            return Db::getInstance()->execute(
                            'DELETE FROM `' . _DB_PREFIX_ . 'customization`
				WHERE `id_customization` = ' . (int) $id_customization
            );
        }

        return true;
    }

    public static function getTotalCart($id_cart, $use_tax_display = false, $type = CART::BOTH) {
        $cart = new Cart($id_cart);
        if (!Validate::isLoadedObject($cart))
            die(Tools::displayError());

        $with_taxes = $use_tax_display ? $cart->_taxCalculationMethod != PS_TAX_EXC : true;
        return Tools::displayPrice($cart->getOrderTotal($with_taxes, $type), Currency::getCurrencyInstance((int) $cart->id_currency), false);
    }

    public static function getOrderTotalUsingTaxCalculationMethod($id_cart) {
        return Cart::getTotalCart($id_cart, true);
    }

    /**
     * This function returns the total cart amount
     *
     * Possible values for $type:
     * Cart::ONLY_PRODUCTS
     * Cart::ONLY_DISCOUNTS
     * Cart::BOTH
     * Cart::BOTH_WITHOUT_SHIPPING
     * Cart::ONLY_SHIPPING
     * Cart::ONLY_WRAPPING
     * Cart::ONLY_PRODUCTS_WITHOUT_SHIPPING
     * Cart::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING
     *
     * @param boolean $withTaxes With or without taxes
     * @param integer $type Total type
     * @param boolean $use_cache Allow using cache of the method CartRule::getContextualValue
     * @return float Order total
     */
    public function getOrderTotal($with_taxes = true, $type = Cart::BOTH, $products = null, $id_carrier = null, $use_cache = true) {
        if (!$this->id)
            return 0;

        $type = (int) $type;
        $array_type = array(
            Cart::ONLY_PRODUCTS,
            Cart::ONLY_DISCOUNTS,
            Cart::BOTH,
            Cart::BOTH_WITHOUT_SHIPPING,
            Cart::ONLY_SHIPPING,
            Cart::ONLY_WRAPPING,
            Cart::ONLY_PRODUCTS_WITHOUT_SHIPPING,
            Cart::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING,
        );

        // Define virtual context to prevent case where the cart is not the in the global context
        $virtual_context = Context::getContext()->cloneContext();
        $virtual_context->cart = $this;

        if (!in_array($type, $array_type))
            die(Tools::displayError());

        $with_shipping = in_array($type, array(Cart::BOTH, Cart::ONLY_SHIPPING));

        // if cart rules are not used
        if ($type == Cart::ONLY_DISCOUNTS && !CartRule::isFeatureActive())
            return 0;

        // no shipping cost if is a cart with only virtuals products
        $virtual = $this->isVirtualCart();
        if ($virtual && $type == Cart::ONLY_SHIPPING)
            return 0;

        if ($virtual && $type == Cart::BOTH)
            $type = Cart::BOTH_WITHOUT_SHIPPING;

        if ($with_shipping) {
            if (is_null($products) && is_null($id_carrier))
                $shipping_fees = $this->getTotalShippingCost(null, (boolean) $with_taxes);
            else
                $shipping_fees = $this->getPackageShippingCost($id_carrier, (int) $with_taxes, null, $products);
        }
        else
            $shipping_fees = 0;

        if ($type == Cart::ONLY_PRODUCTS_WITHOUT_SHIPPING)
            $type = Cart::ONLY_PRODUCTS;

        $param_product = true;
        if (is_null($products)) {
            $param_product = false;
            $products = $this->getProducts();
        }

        if ($type == Cart::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING) {
            foreach ($products as $key => $product)
                if ($product['is_virtual'])
                    unset($products[$key]);
            $type = Cart::ONLY_PRODUCTS;
        }

        $order_total = 0;
        if (Tax::excludeTaxeOption())
            $with_taxes = false;

        foreach ($products as $product) { // products refer to the cart details
            if ($virtual_context->shop->id != $product['id_shop'])
                $virtual_context->shop = new Shop((int) $product['id_shop']);

            if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice')
                $address_id = (int) $this->id_address_invoice;
            else
                $address_id = (int) $product['id_address_delivery']; // Get delivery address of the product from the cart
            if (!Address::addressExists($address_id))
                $address_id = null;

            if ($this->_taxCalculationMethod == PS_TAX_EXC) {
                // Here taxes are computed only once the quantity has been applied to the product price
                //echo ("hung.nguyen 1----");
                $price = Product::getPriceStatic(
                                (int) $product['id_product'], false, (int) $product['id_product_attribute'], 2, null, false, true, $product['cart_quantity'], false, (int) $this->id_customer ? (int) $this->id_customer : null, (int) $this->id, $address_id, $null, true, true, $virtual_context
                );

                $total_ecotax = $product['ecotax'] * (int) $product['cart_quantity'];
                $total_price = $price * (int) $product['cart_quantity'];

                if ($with_taxes) {
                    $product_tax_rate = (float) Tax::getProductTaxRate((int) $product['id_product'], (int) $address_id, $virtual_context);
                    $product_eco_tax_rate = Tax::getProductEcotaxRate((int) $address_id);

                    $total_price = ($total_price - $total_ecotax) * (1 + $product_tax_rate / 100);
                    $total_ecotax = $total_ecotax * (1 + $product_eco_tax_rate / 100);
                    $total_price = Tools::ps_round($total_price + $total_ecotax, 2);
                }
            } else {
                if ($with_taxes)
                {
                    //echo ("hung.nguyen 2----".(int) $product['id_product_attribute']."---");
                    $price = Product::getPriceStatic(
                                    (int) $product['id_product'], true, (int) $product['id_product_attribute'], 2, null, false, true, $product['cart_quantity'], false, ((int) $this->id_customer ? (int) $this->id_customer : null), (int) $this->id, ((int) $address_id ? (int) $address_id : null), $null, true, true, $virtual_context
                    );
                }
                else
                {
                    //echo ("hung.nguyen 3----");
                    $price = Product::getPriceStatic(
                                    (int) $product['id_product'], false, (int) $product['id_product_attribute'], 2, null, false, true, $product['cart_quantity'], false, ((int) $this->id_customer ? (int) $this->id_customer : null), (int) $this->id, ((int) $address_id ? (int) $address_id : null), $null, true, true, $virtual_context
                    );
                }
                $total_price = Tools::ps_round($price * (int) $product['cart_quantity'], 2);
            }
            $order_total += $total_price;
        }

        $order_total_products = $order_total;

        if ($type == Cart::ONLY_DISCOUNTS)
            $order_total = 0;

        // Wrapping Fees
        $wrapping_fees = 0;
        if ($this->gift)
            $wrapping_fees = Tools::convertPrice(Tools::ps_round($this->getGiftWrappingPrice($with_taxes), 2), Currency::getCurrencyInstance((int) $this->id_currency));

        $order_total_discount = 0;
        if (!in_array($type, array(Cart::ONLY_SHIPPING, Cart::ONLY_PRODUCTS)) && CartRule::isFeatureActive()) {
            // First, retrieve the cart rules associated to this "getOrderTotal"
            if ($with_shipping)
                $cart_rules = $this->getCartRules(CartRule::FILTER_ACTION_ALL);
            else {
                $cart_rules = $this->getCartRules(CartRule::FILTER_ACTION_REDUCTION);
                // Cart Rules array are merged manually in order to avoid doubles
                foreach ($this->getCartRules(CartRule::FILTER_ACTION_GIFT) as $tmp_cart_rule) {
                    $flag = false;
                    foreach ($cart_rules as $cart_rule)
                        if ($tmp_cart_rule['id_cart_rule'] == $cart_rule['id_cart_rule'])
                            $flag = true;
                    if (!$flag)
                        $cart_rules[] = $tmp_cart_rule;
                }
            }

            $id_address_delivery = 0;
            if (isset($products[0]))
                $id_address_delivery = (is_null($products) ? $this->id_address_delivery : $products[0]['id_address_delivery']);
            $package = array('id_carrier' => $id_carrier, 'id_address' => $id_address_delivery, 'products' => $products);

            // Then, calculate the contextual value for each one
            foreach ($cart_rules as $cart_rule) {
                // If the cart rule offers free shipping, add the shipping cost
                if ($with_shipping && $cart_rule['obj']->free_shipping)
                    $order_total_discount += Tools::ps_round($cart_rule['obj']->getContextualValue($with_taxes, $virtual_context, CartRule::FILTER_ACTION_SHIPPING, ($param_product ? $package : null), $use_cache), 2);

                // If the cart rule is a free gift, then add the free gift value only if the gift is in this package
                if ((int) $cart_rule['obj']->gift_product) {
                    $in_order = false;
                    if (is_null($products))
                        $in_order = true;
                    else
                        foreach ($products as $product)
                            if ($cart_rule['obj']->gift_product == $product['id_product'] && $cart_rule['obj']->gift_product_attribute == $product['id_product_attribute'])
                                $in_order = true;

                    if ($in_order)
                        $order_total_discount += $cart_rule['obj']->getContextualValue($with_taxes, $virtual_context, CartRule::FILTER_ACTION_GIFT, $package, $use_cache);
                }

                // If the cart rule offers a reduction, the amount is prorated (with the products in the package)
                if ($cart_rule['obj']->reduction_percent > 0 || $cart_rule['obj']->reduction_amount > 0)
                    $order_total_discount += Tools::ps_round($cart_rule['obj']->getContextualValue($with_taxes, $virtual_context, CartRule::FILTER_ACTION_REDUCTION, $package, $use_cache), 2);
            }

            $order_total_discount = min(Tools::ps_round($order_total_discount, 2), $wrapping_fees + $order_total_products + $shipping_fees);
            $order_total -= $order_total_discount;
        }

        if ($type == Cart::ONLY_SHIPPING)
            return $shipping_fees;

        if ($type == Cart::ONLY_WRAPPING)
            return $wrapping_fees;

        if ($type == Cart::BOTH)
            $order_total += $shipping_fees + $wrapping_fees;

        if ($order_total < 0 && $type != Cart::ONLY_DISCOUNTS)
            return 0;

        if ($type == Cart::ONLY_DISCOUNTS)
            return $order_total_discount;

        return Tools::ps_round((float) $order_total, 2);
    }

    /**
     * Get the gift wrapping price
     * @param boolean $with_taxes With or without taxes
     * @return gift wrapping price
     */
    public function getGiftWrappingPrice($with_taxes = true, $id_address = null) {
        static $address = null;

        if ($id_address === null)
            $id_address = (int) $this->{Configuration::get('PS_TAX_ADDRESS_TYPE')};

        if ($address === null)
            $address = Address::initialize($id_address);

        $wrapping_fees = (float) Configuration::get('PS_GIFT_WRAPPING_PRICE');
        if ($with_taxes && $wrapping_fees > 0) {
            $tax_manager = TaxManagerFactory::getManager($address, (int) Configuration::get('PS_GIFT_WRAPPING_TAX_RULES_GROUP'));
            $tax_calculator = $tax_manager->getTaxCalculator();
            $wrapping_fees = $tax_calculator->addTaxes($wrapping_fees);
        }

        return $wrapping_fees;
    }

    /**
     * Get the number of packages
     * 
     * @return int number of packages
     */
    public function getNbOfPackages() {
        static $nb_packages = 0;

        if (!$nb_packages)
            foreach ($this->getPackageList() as $by_address)
                $nb_packages += count($by_address);

        return $nb_packages;
    }

    /**
     * Get products grouped by package and by addresses to be sent individualy (one package = one shipping cost).
     *
     * @return array array(
     *                   0 => array( // First address
     *                       0 => array(  // First package
     *                           'product_list' => array(...),
     *                           'carrier_list' => array(...),
     *                           'id_warehouse' => array(...),
     *                       ),
     *                   ),
     *               );
     * @todo Add avaibility check
     */
    public function getPackageList($flush = false) {
        static $cache = false;
        if ($cache !== false && !$flush)
            return $cache;

        $product_list = $this->getProducts();
        // Step 1 : Get product informations (warehouse_list and carrier_list), count warehouse
        // Determine the best warehouse to determine the packages
        // For that we count the number of time we can use a warehouse for a specific delivery address
        $warehouse_count_by_address = array();
        $warehouse_carrier_list = array();

        $stock_management_active = Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT');

        foreach ($product_list as &$product) {
            if ((int) $product['id_address_delivery'] == 0)
                $product['id_address_delivery'] = (int) $this->id_address_delivery;

            if (!isset($warehouse_count_by_address[$product['id_address_delivery']]))
                $warehouse_count_by_address[$product['id_address_delivery']] = array();

            $product['warehouse_list'] = array();

            if ($stock_management_active &&
                    ((int) $product['advanced_stock_management'] == 1 || Pack::usesAdvancedStockManagement((int) $product['id_product']))) {
                $warehouse_list = Warehouse::getProductWarehouseList($product['id_product'], $product['id_product_attribute'], $this->id_shop);
                if (count($warehouse_list) == 0)
                    $warehouse_list = Warehouse::getProductWarehouseList($product['id_product'], $product['id_product_attribute']);
                // Does the product is in stock ?
                // If yes, get only warehouse where the product is in stock

                $warehouse_in_stock = array();
                $manager = StockManagerFactory::getManager();

                foreach ($warehouse_list as $key => $warehouse) {
                    $product_real_quantities = $manager->getProductRealQuantities(
                            $product['id_product'], $product['id_product_attribute'], array($warehouse['id_warehouse']), true
                    );

                    if ($product_real_quantities > 0 || Pack::isPack((int) $product['id_product']))
                        $warehouse_in_stock[] = $warehouse;
                }

                if (!empty($warehouse_in_stock)) {
                    $warehouse_list = $warehouse_in_stock;
                    $product['in_stock'] = true;
                }
                else
                    $product['in_stock'] = false;
            }
            else {
                //simulate default warehouse
                $warehouse_list = array(0);
                $product['in_stock'] = StockAvailable::getQuantityAvailableByProduct($product['id_product'], $product['id_product_attribute']) > 0;
            }

            foreach ($warehouse_list as $warehouse) {
                if (!isset($warehouse_carrier_list[$warehouse['id_warehouse']])) {
                    $warehouse_object = new Warehouse($warehouse['id_warehouse']);
                    $warehouse_carrier_list[$warehouse['id_warehouse']] = $warehouse_object->getCarriers();
                }

                $product['warehouse_list'][] = $warehouse['id_warehouse'];
                if (!isset($warehouse_count_by_address[$product['id_address_delivery']][$warehouse['id_warehouse']]))
                    $warehouse_count_by_address[$product['id_address_delivery']][$warehouse['id_warehouse']] = 0;

                $warehouse_count_by_address[$product['id_address_delivery']][$warehouse['id_warehouse']]++;
            }
        }

        arsort($warehouse_count_by_address);

        // Step 2 : Group product by warehouse
        $grouped_by_warehouse = array();
        foreach ($product_list as &$product) {
            if (!isset($grouped_by_warehouse[$product['id_address_delivery']]))
                $grouped_by_warehouse[$product['id_address_delivery']] = array(
                    'in_stock' => array(),
                    'out_of_stock' => array(),
                );

            $product['carrier_list'] = array();
            $id_warehouse = 0;
            foreach ($warehouse_count_by_address[$product['id_address_delivery']] as $id_war => $val) {
                $product['carrier_list'] = array_merge($product['carrier_list'], Carrier::getAvailableCarrierList(new Product($product['id_product']), $id_war, $product['id_address_delivery'], null, $this));
                if (in_array((int) $id_war, $product['warehouse_list']) && $id_warehouse == 0)
                    $id_warehouse = $id_war;
            }

            if (!isset($grouped_by_warehouse[$product['id_address_delivery']]['in_stock'][$id_warehouse])) {
                $grouped_by_warehouse[$product['id_address_delivery']]['in_stock'][$id_warehouse] = array();
                $grouped_by_warehouse[$product['id_address_delivery']]['out_of_stock'][$id_warehouse] = array();
            }

            if (!$this->allow_seperated_package)
                $key = 'in_stock';
            else
                $key = (!$product['out_of_stock']) ? 'in_stock' : 'out_of_stock';

            if (empty($product['carrier_list']))
                $product['carrier_list'] = array(0);

            $grouped_by_warehouse[$product['id_address_delivery']][$key][$id_warehouse][] = $product;
        }

        // Step 3 : grouped product from grouped_by_warehouse by available carriers
        $grouped_by_carriers = array();
        foreach ($grouped_by_warehouse as $id_address_delivery => $products_in_stock_list) {
            if (!isset($grouped_by_carriers[$id_address_delivery]))
                $grouped_by_carriers[$id_address_delivery] = array(
                    'in_stock' => array(),
                    'out_of_stock' => array(),
                );
            foreach ($products_in_stock_list as $key => $warehouse_list) {
                if (!isset($grouped_by_carriers[$id_address_delivery][$key]))
                    $grouped_by_carriers[$id_address_delivery][$key] = array();
                foreach ($warehouse_list as $id_warehouse => $product_list) {
                    if (!isset($grouped_by_carriers[$id_address_delivery][$key][$id_warehouse]))
                        $grouped_by_carriers[$id_address_delivery][$key][$id_warehouse] = array();

                    foreach ($product_list as $product) {
                        $package_carriers_key = implode(',', $product['carrier_list']);

                        if (!isset($grouped_by_carriers[$id_address_delivery][$key][$id_warehouse][$package_carriers_key]))
                            $grouped_by_carriers[$id_address_delivery][$key][$id_warehouse][$package_carriers_key] = array(
                                'product_list' => array(),
                                'carrier_list' => $product['carrier_list'],
                                'warehouse_list' => $product['warehouse_list']
                            );

                        $grouped_by_carriers[$id_address_delivery][$key][$id_warehouse][$package_carriers_key]['product_list'][] = $product;
                    }
                }
            }
        }

        $package_list = array();
        // Step 4 : merge product from grouped_by_carriers into $package to minimize the number of package
        foreach ($grouped_by_carriers as $id_address_delivery => $products_in_stock_list) {
            if (!isset($package_list[$id_address_delivery]))
                $package_list[$id_address_delivery] = array(
                    'in_stock' => array(),
                    'out_of_stock' => array(),
                );

            foreach ($products_in_stock_list as $key => $warehouse_list) {
                if (!isset($package_list[$id_address_delivery][$key]))
                    $package_list[$id_address_delivery][$key] = array();
                // Count occurance of each carriers to minimize the number of packages
                $carrier_count = array();
                foreach ($warehouse_list as $id_warehouse => $products_grouped_by_carriers) {
                    foreach ($products_grouped_by_carriers as $data) {
                        foreach ($data['carrier_list'] as $id_carrier) {
                            if (!isset($carrier_count[$id_carrier]))
                                $carrier_count[$id_carrier] = 0;
                            $carrier_count[$id_carrier]++;
                        }
                    }
                }
                arsort($carrier_count);
                foreach ($warehouse_list as $id_warehouse => $products_grouped_by_carriers) {
                    if (!isset($package_list[$id_address_delivery][$key][$id_warehouse]))
                        $package_list[$id_address_delivery][$key][$id_warehouse] = array();
                    foreach ($products_grouped_by_carriers as $data) {
                        foreach ($carrier_count as $id_carrier => $rate) {
                            if (in_array($id_carrier, $data['carrier_list'])) {
                                if (!isset($package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier]))
                                    $package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier] = array(
                                        'carrier_list' => $data['carrier_list'],
                                        'warehouse_list' => $data['warehouse_list'],
                                        'product_list' => array(),
                                    );
                                $package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier]['carrier_list'] =
                                        array_intersect($package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier]['carrier_list'], $data['carrier_list']);

                                $package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier]['product_list'] =
                                        array_merge($package_list[$id_address_delivery][$key][$id_warehouse][$id_carrier]['product_list'], $data['product_list']);

                                break;
                            }
                        }
                    }
                }
            }
        }

        // Step 5 : Reduce depth of $package_list
        $final_package_list = array();
        foreach ($package_list as $id_address_delivery => $products_in_stock_list) {
            if (!isset($final_package_list[$id_address_delivery]))
                $final_package_list[$id_address_delivery] = array();

            foreach ($products_in_stock_list as $key => $warehouse_list)
                foreach ($warehouse_list as $id_warehouse => $products_grouped_by_carriers)
                    foreach ($products_grouped_by_carriers as $data) {
                        $final_package_list[$id_address_delivery][] = array(
                            'product_list' => $data['product_list'],
                            'carrier_list' => $data['carrier_list'],
                            'warehouse_list' => $data['warehouse_list'],
                            'id_warehouse' => $id_warehouse,
                        );
                    }
        }
        $cache = $final_package_list;
        return $final_package_list;
    }

    public function getPackageIdWarehouse($package, $id_carrier = null) {
        if ($id_carrier === null)
            if (isset($package['id_carrier']))
                $id_carrier = (int) $package['id_carrier'];

        if ($id_carrier == null)
            return $package['id_warehouse'];

        foreach ($package['warehouse_list'] as $id_warehouse) {
            $warehouse = new Warehouse((int) $id_warehouse);
            $available_warehouse_carriers = $warehouse->getCarriers();
            if (in_array($id_carrier, $available_warehouse_carriers))
                return (int) $id_warehouse;
        }
        return 0;
    }

    /**
     * Get all deliveries options available for the current cart
     * @param Country $default_country
     * @param boolean $flush Force flushing cache
     *
     * @return array array(
     *                   0 => array( // First address
     *                       '12,' => array(  // First delivery option available for this address
     *                           carrier_list => array(
     *                               12 => array( // First carrier for this option
     *                                   'instance' => Carrier Object,
     *                                   'logo' => <url to the carriers logo>,
     *                                   'price_with_tax' => 12.4,
     *                                   'price_without_tax' => 12.4,
     *                                   'package_list' => array(
     *                                       1,
     *                                       3,
     *                                   ),
     *                               ),
     *                           ),
     *                           is_best_grade => true, // Does this option have the biggest grade (quick shipping) for this shipping address
     *                           is_best_price => true, // Does this option have the lower price for this shipping address
     *                           unique_carrier => true, // Does this option use a unique carrier
     *                           total_price_with_tax => 12.5,
     *                           total_price_without_tax => 12.5,
     *                           position => 5, // Average of the carrier position
     *                       ),
     *                   ),
     *               );
     *               If there are no carriers available for an address, return an empty  array
     */
    public function getDeliveryOptionList(Country $default_country = null, $flush = false) {
        static $cache = null;
        if ($cache !== null && !$flush)
            return $cache;

        $delivery_option_list = array();
        $carriers_price = array();
        $carrier_collection = array();
        $package_list = $this->getPackageList();

        // Foreach addresses
        foreach ($package_list as $id_address => $packages) {
            // Initialize vars
            $delivery_option_list[$id_address] = array();
            $carriers_price[$id_address] = array();
            $common_carriers = null;
            $best_price_carriers = array();
            $best_grade_carriers = array();
            $carriers_instance = array();

            // Get country
            if ($id_address) {
                $address = new Address($id_address);
                $country = new Country($address->id_country);
            }
            else
                $country = $default_country;

            // Foreach packages, get the carriers with best price, best position and best grade
            foreach ($packages as $id_package => $package) {
                // No carriers available
                if (count($package['carrier_list']) == 1 && current($package['carrier_list']) == 0) {
                    $cache = array();
                    return $cache;
                }

                $carriers_price[$id_address][$id_package] = array();

                // Get all common carriers for each packages to the same address
                if (is_null($common_carriers))
                    $common_carriers = $package['carrier_list'];
                else
                    $common_carriers = array_intersect($common_carriers, $package['carrier_list']);

                $best_price = null;
                $best_price_carrier = null;
                $best_grade = null;
                $best_grade_carrier = null;

                // Foreach carriers of the package, calculate his price, check if it the best price, position and grade
                foreach ($package['carrier_list'] as $id_carrier) {
                    if (!isset($carriers_instance[$id_carrier]))
                        $carriers_instance[$id_carrier] = new Carrier($id_carrier);

                    $price_with_tax = $this->getPackageShippingCost($id_carrier, true, $country, $package['product_list']);
                    $price_without_tax = $this->getPackageShippingCost($id_carrier, false, $country, $package['product_list']);
                    if (is_null($best_price) || $price_with_tax < $best_price) {
                        $best_price = $price_with_tax;
                        $best_price_carrier = $id_carrier;
                    }
                    $carriers_price[$id_address][$id_package][$id_carrier] = array(
                        'without_tax' => $price_without_tax,
                        'with_tax' => $price_with_tax);

                    $grade = $carriers_instance[$id_carrier]->grade;
                    if (is_null($best_grade) || $grade > $best_grade) {
                        $best_grade = $grade;
                        $best_grade_carrier = $id_carrier;
                    }
                }

                $best_price_carriers[$id_package] = $best_price_carrier;
                $best_grade_carriers[$id_package] = $best_grade_carrier;
            }

            // Reset $best_price_carrier, it's now an array
            $best_price_carrier = array();
            $key = '';

            // Get the delivery option with the lower price
            foreach ($best_price_carriers as $id_package => $id_carrier) {
                $key .= $id_carrier . ',';
                if (!isset($best_price_carrier[$id_carrier]))
                    $best_price_carrier[$id_carrier] = array(
                        'price_with_tax' => 0,
                        'price_without_tax' => 0,
                        'package_list' => array(),
                        'product_list' => array(),
                    );
                $best_price_carrier[$id_carrier]['price_with_tax'] += $carriers_price[$id_address][$id_package][$id_carrier]['with_tax'];
                $best_price_carrier[$id_carrier]['price_without_tax'] += $carriers_price[$id_address][$id_package][$id_carrier]['without_tax'];
                $best_price_carrier[$id_carrier]['package_list'][] = $id_package;
                $best_price_carrier[$id_carrier]['product_list'] = array_merge($best_price_carrier[$id_carrier]['product_list'], $packages[$id_package]['product_list']);
                $best_price_carrier[$id_carrier]['instance'] = $carriers_instance[$id_carrier];
            }

            // Add the delivery option with best price as best price
            $delivery_option_list[$id_address][$key] = array(
                'carrier_list' => $best_price_carrier,
                'is_best_price' => true,
                'is_best_grade' => false,
                'unique_carrier' => (count($best_price_carrier) <= 1)
            );

            // Reset $best_grade_carrier, it's now an array
            $best_grade_carrier = array();
            $key = '';

            // Get the delivery option with the best grade
            foreach ($best_grade_carriers as $id_package => $id_carrier) {
                $key .= $id_carrier . ',';
                if (!isset($best_grade_carrier[$id_carrier]))
                    $best_grade_carrier[$id_carrier] = array(
                        'price_with_tax' => 0,
                        'price_without_tax' => 0,
                        'package_list' => array(),
                        'product_list' => array(),
                    );
                $best_grade_carrier[$id_carrier]['price_with_tax'] += $carriers_price[$id_address][$id_package][$id_carrier]['with_tax'];
                $best_grade_carrier[$id_carrier]['price_without_tax'] += $carriers_price[$id_address][$id_package][$id_carrier]['without_tax'];
                $best_grade_carrier[$id_carrier]['package_list'][] = $id_package;
                $best_grade_carrier[$id_carrier]['product_list'] = array_merge($best_grade_carrier[$id_carrier]['product_list'], $packages[$id_package]['product_list']);
                $best_grade_carrier[$id_carrier]['instance'] = $carriers_instance[$id_carrier];
            }

            // Add the delivery option with best grade as best grade
            if (!isset($delivery_option_list[$id_address][$key]))
                $delivery_option_list[$id_address][$key] = array(
                    'carrier_list' => $best_grade_carrier,
                    'is_best_price' => false,
                    'unique_carrier' => (count($best_grade_carrier) <= 1)
                );
            $delivery_option_list[$id_address][$key]['is_best_grade'] = true;

            // Get all delivery options with a unique carrier
            foreach ($common_carriers as $id_carrier) {
                $price = 0;
                $key = '';
                $package_list = array();
                $product_list = array();
                $total_price_with_tax = 0;
                $total_price_without_tax = 0;
                $price_with_tax = 0;
                $price_without_tax = 0;

                foreach ($packages as $id_package => $package) {
                    $key .= $id_carrier . ',';
                    $price_with_tax += $carriers_price[$id_address][$id_package][$id_carrier]['with_tax'];
                    $price_without_tax += $carriers_price[$id_address][$id_package][$id_carrier]['without_tax'];
                    $package_list[] = $id_package;
                    $product_list = array_merge($product_list, $package['product_list']);
                }

                if (!isset($delivery_option_list[$id_address][$key]))
                    $delivery_option_list[$id_address][$key] = array(
                        'is_best_price' => false,
                        'is_best_grade' => false,
                        'unique_carrier' => true,
                        'carrier_list' => array(
                            $id_carrier => array(
                                'price_with_tax' => $price_with_tax,
                                'price_without_tax' => $price_without_tax,
                                'instance' => $carriers_instance[$id_carrier],
                                'package_list' => $package_list,
                                'product_list' => $product_list,
                            )
                        )
                    );
                else
                    $delivery_option_list[$id_address][$key]['unique_carrier'] = (count($delivery_option_list[$id_address][$key]['carrier_list']) <= 1);
            }
        }

        // For each delivery options :
        //    - Set the carrier list
        //    - Calculate the price
        //    - Calculate the average position
        foreach ($delivery_option_list as $id_address => $delivery_option)
            foreach ($delivery_option as $key => $value) {
                $total_price_with_tax = 0;
                $total_price_without_tax = 0;
                $position = 0;
                foreach ($value['carrier_list'] as $id_carrier => $data) {
                    $total_price_with_tax += $data['price_with_tax'];
                    $total_price_without_tax += $data['price_without_tax'];

                    if (!isset($carrier_collection[$id_carrier]))
                        $carrier_collection[$id_carrier] = new Carrier($id_carrier);
                    $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['instance'] = $carrier_collection[$id_carrier];

                    if (file_exists(_PS_SHIP_IMG_DIR_ . $id_carrier . '.jpg'))
                        $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['logo'] = _THEME_SHIP_DIR_ . $id_carrier . '.jpg';
                    else
                        $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['logo'] = false;

                    $position += $carrier_collection[$id_carrier]->position;
                }
                $delivery_option_list[$id_address][$key]['total_price_with_tax'] = $total_price_with_tax;
                $delivery_option_list[$id_address][$key]['total_price_without_tax'] = $total_price_without_tax;
                $delivery_option_list[$id_address][$key]['position'] = $position / count($value['carrier_list']);
            }

        // Sort delivery option list
        foreach ($delivery_option_list as &$array)
            uasort($array, array('Cart', 'sortDeliveryOptionList'));

        $cache = $delivery_option_list;
        return $delivery_option_list;
    }

    /**
     * 
     * Sort list of option delivery by parameters define in the BO
     * @param $option1
     * @param $option2
     * @return int -1 if $option 1 must be placed before and 1 if the $option1 must be placed after the $option2
     */
    public static function sortDeliveryOptionList($option1, $option2) {
        static $order_by_price = null;
        static $order_way = null;
        if (is_null($order_by_price))
            $order_by_price = !Configuration::get('PS_CARRIER_DEFAULT_SORT');
        if (is_null($order_way))
            $order_way = Configuration::get('PS_CARRIER_DEFAULT_ORDER');

        if ($order_by_price)
            if ($order_way)
                return ($option1['total_price_with_tax'] < $option2['total_price_with_tax']) * 2 - 1; // return -1 or 1
            else
                return ($option1['total_price_with_tax'] >= $option2['total_price_with_tax']) * 2 - 1; // return -1 or 1
                else
        if ($order_way)
            return ($option1['position'] < $option2['position']) * 2 - 1; // return -1 or 1
        else
            return ($option1['position'] >= $option2['position']) * 2 - 1; // return -1 or 1
    }

    public function carrierIsSelected($id_carrier, $id_address) {
        $delivery_option = $this->getDeliveryOption();
        $delivery_option_list = $this->getDeliveryOptionList();

        if (!isset($delivery_option[$id_address]))
            return false;

        if (!isset($delivery_option_list[$id_address][$delivery_option[$id_address]]))
            return false;

        if (!in_array($id_carrier, array_keys($delivery_option_list[$id_address][$delivery_option[$id_address]]['carrier_list'])))
            return false;

        return true;
    }

    /**
     * Get all deliveries options available for the current cart formated like Carriers::getCarriersForOrder
     * This method was wrote for retrocompatibility with 1.4 theme
     * New theme need to use Cart::getDeliveryOptionList() to generate carriers option in the checkout process
     *
     * @since 1.5.0
     *
     * @param Country $default_country
     * @param boolean $flush Force flushing cache
     *
     */
    public function simulateCarriersOutput(Country $default_country = null, $flush = false) {
        static $cache = false;
        if ($cache !== false && !$flush)
            return $cache;

        $delivery_option_list = $this->getDeliveryOptionList($default_country, $flush);

        // This method cannot work if there is multiple address delivery
        if (count($delivery_option_list) > 1 || empty($delivery_option_list))
            return array();

        $carriers = array();
        foreach (reset($delivery_option_list) as $key => $option) {
            $price = $option['total_price_with_tax'];
            $price_tax_exc = $option['total_price_without_tax'];

            if ($option['unique_carrier']) {
                $carrier = reset($option['carrier_list']);
                $name = $carrier['instance']->name;
                $img = $carrier['logo'];
                $delay = $carrier['instance']->delay;
                $delay = isset($delay[Context::getContext()->language->id]) ? $delay[Context::getContext()->language->id] : $delay[(int) Configuration::get('PS_LANG_DEFAULT')];
            } else {
                $nameList = array();
                foreach ($option['carrier_list'] as $carrier)
                    $nameList[] = $carrier['instance']->name;
                $name = join(' -', $nameList);
                $img = ''; // No images if multiple carriers
                $delay = '';
            }
            $carriers[] = array(
                'name' => $name,
                'img' => $img,
                'delay' => $delay,
                'price' => $price,
                'price_tax_exc' => $price_tax_exc,
                'id_carrier' => Cart::intifier($key), // Need to translate to an integer for retrocompatibility reason, in 1.4 template we used intval
                'is_module' => false,
            );
        }
        return $carriers;
    }

    public function simulateCarrierSelectedOutput() {
        $delivery_option = $this->getDeliveryOption();

        if (count($delivery_option) > 1 || empty($delivery_option))
            return 0;

        return Cart::intifier(reset($delivery_option));
    }

    /**
     * Translate a string option_delivery identifier ('24,3,') in a int (3240002000)
     *
     * The  option_delivery identifier is a list of integers separated by a ','.
     * This method replace the delimiter by a sequence of '0'.
     * The size of this sequence is fixed by the first digit of the return
     *
     * @return int
     */
    public static function intifier($string, $delimiter = ',') {
        $elm = explode($delimiter, $string);
        $max = max($elm);
        return strlen($max) . implode(str_repeat('0', strlen($max) + 1), $elm);
    }

    /**
     * Translate a int option_delivery identifier (3240002000) in a string ('24,3,')
     */
    public static function desintifier($int, $delimiter = ',') {
        $delimiter_len = $int[0];
        $int = strrev(substr($int, 1));
        $elm = explode(str_repeat('0', $delimiter_len + 1), $int);
        return strrev(implode($delimiter, $elm));
    }

    /**
     * Does the cart use multiple address
     * @return boolean
     */
    public function isMultiAddressDelivery() {
        static $cache = null;

        if (is_null($cache)) {
            $sql = new DbQuery();
            $sql->select('count(distinct id_address_delivery)');
            $sql->from('cart_product', 'cp');
            $sql->where('id_cart = ' . (int) $this->id);

            $cache = Db::getInstance()->getValue($sql) > 1;
        }
        return $cache;
    }

    /**
     * Get all delivery addresses object for the current cart
     */
    public function getAddressCollection() {
        $collection = array();
        $result = Db::getInstance()->executeS(
                'SELECT DISTINCT `id_address_delivery`
			FROM `' . _DB_PREFIX_ . 'cart_product`
			WHERE id_cart = ' . (int) $this->id
        );

        $result[] = array('id_address_delivery' => (int) $this->id_address_delivery);

        foreach ($result as $row)
            if ((int) $row['id_address_delivery'] != 0)
                $collection[(int) $row['id_address_delivery']] = new Address((int) $row['id_address_delivery']);

        return $collection;
    }

    /**
     * Set the delivery option and id_carrier, if there is only one carrier
     */
    public function setDeliveryOption($delivery_option = null) {
        if (empty($delivery_option) || count($delivery_option) == 0) {
            $this->delivery_option = '';
            $this->id_carrier = 0;
            return;
        }

        $delivery_option_list = $this->getDeliveryOptionList(null, true);

        foreach ($delivery_option_list as $id_address => $options)
            if (!isset($delivery_option[$id_address]))
                foreach ($options as $key => $option)
                    if ($option['is_best_price']) {
                        $delivery_option[$id_address] = $key;
                        break;
                    }

        if (count($delivery_option) == 1)
            $this->id_carrier = $this->getIdCarrierFromDeliveryOption($delivery_option);

        $this->delivery_option = serialize($delivery_option);
    }

    protected function getIdCarrierFromDeliveryOption($delivery_option) {
        $delivery_option_list = $this->getDeliveryOptionList();
        foreach ($delivery_option as $key => $value)
            if (isset($delivery_option_list[$key]) && isset($delivery_option_list[$key][$value]))
                if (count($delivery_option_list[$key][$value]['carrier_list']) == 1)
                    return current(array_keys($delivery_option_list[$key][$value]['carrier_list']));

        return 0;
    }

    /**
     * Get the delivery option seleted, or if no delivery option was selected, the cheapest option for each address
     * @return array delivery option
     */
    public function getDeliveryOption($default_country = null, $dontAutoSeletectOptions = false, $use_cache = true) {
        static $cache = array(0 => false, 1 => false);
        if ($cache[(int) $dontAutoSeletectOptions] !== false && $use_cache)
            return $cache[(int) $dontAutoSeletectOptions];

        $delivery_option_list = $this->getDeliveryOptionList($default_country);

        // The delivery option was selected
        if (isset($this->delivery_option) && $this->delivery_option != '') {
            $delivery_option = Tools::unSerialize($this->delivery_option);
            $validated = true;
            foreach ($delivery_option as $id_address => $key)
                if (!isset($delivery_option_list[$id_address][$key])) {
                    $validated = false;
                    break;
                }

            if ($validated) {
                $cache[(int) $dontAutoSeletectOptions] = $delivery_option;
                return $delivery_option;
            }
        }

        if ($dontAutoSeletectOptions)
            return false;

        // No delivery option selected or delivery option selected is not valid, get the better for all options
        $delivery_option = array();
        foreach ($delivery_option_list as $id_address => $options) {
            foreach ($options as $key => $option)
                if (Configuration::get('PS_CARRIER_DEFAULT') == -1 && $option['is_best_price']) {
                    $delivery_option[$id_address] = $key;
                    break;
                } elseif (Configuration::get('PS_CARRIER_DEFAULT') == -2 && $option['is_best_grade']) {
                    $delivery_option[$id_address] = $key;
                    break;
                } elseif ($option['unique_carrier'] && in_array(Configuration::get('PS_CARRIER_DEFAULT'), array_keys($option['carrier_list']))) {
                    $delivery_option[$id_address] = $key;
                    break;
                }

            reset($options);
            if (!isset($delivery_option[$id_address]))
                $delivery_option[$id_address] = key($options);
        }

        $cache[(int) $dontAutoSeletectOptions] = $delivery_option;

        return $delivery_option;
    }

    /**
     * Return shipping total for the cart
     *
     * @param array $delivery_option Array of the delivery option for each address
     * @param booleal $use_tax
     * @param Country $default_country
     * @return float Shipping total
     */
    public function getTotalShippingCost($delivery_option = null, $use_tax = true, Country $default_country = null) {
        if (is_null($delivery_option))
            $delivery_option = $this->getDeliveryOption($default_country, false, false);

        $total_shipping = 0;
        $delivery_option_list = $this->getDeliveryOptionList();
        foreach ($delivery_option as $id_address => $key) {
            if (!isset($delivery_option_list[$id_address]) || !isset($delivery_option_list[$id_address][$key]))
                continue;
            if ($use_tax)
                $total_shipping += $delivery_option_list[$id_address][$key]['total_price_with_tax'];
            else
                $total_shipping += $delivery_option_list[$id_address][$key]['total_price_without_tax'];
        }

        return $total_shipping;
    }

    /**
     * Return shipping total of a specific carriers for the cart
     *
     * @param int $id_carrier
     * @param array $delivery_option Array of the delivery option for each address
     * @param booleal $useTax
     * @param Country $default_country
     * @return float Shipping total
     */
    public function getCarrierCost($id_carrier, $useTax = true, Country $default_country = null, $delivery_option = null) {
        if (is_null($delivery_option))
            $delivery_option = $this->getDeliveryOption($default_country);

        $total_shipping = 0;
        $delivery_option_list = $this->getDeliveryOptionList();


        foreach ($delivery_option as $id_address => $key) {
            if (!isset($delivery_option_list[$id_address]) || !isset($delivery_option_list[$id_address][$key]))
                continue;
            if (isset($delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier])) {
                if ($useTax)
                    $total_shipping += $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['price_with_tax'];
                else
                    $total_shipping += $delivery_option_list[$id_address][$key]['carrier_list'][$id_carrier]['price_without_tax'];
            }
        }

        return $total_shipping;
    }

    /**
     * @deprecated 1.5.0, use Cart->getPackageShippingCost()
     */
    public function getOrderShippingCost($id_carrier = null, $use_tax = true, Country $default_country = null, $product_list = null) {
        Tools::displayAsDeprecated();
        return $this->getPackageShippingCost($id_carrier, $use_tax, $default_country, $product_list);
    }

    /**
     * Return package shipping cost
     *
     * @param integer $id_carrier Carrier ID (default : current carrier)
     * @param booleal $use_tax
     * @param Country $default_country
     * @param Array $product_list
     * @param array $product_list List of product concerned by the shipping. If null, all the product of the cart are used to calculate the shipping cost
     *
     * @return float Shipping total
     */
    public function getPackageShippingCost($id_carrier = null, $use_tax = true, Country $default_country = null, $product_list = null) {
        if ($this->isVirtualCart())
            return 0;

        if (!$default_country)
            $default_country = Context::getContext()->country;

        $complete_product_list = $this->getProducts();
        if (is_null($product_list))
            $products = $complete_product_list;
        else
            $products = $product_list;

        if (Configuration::get('PS_TAX_ADDRESS_TYPE') == 'id_address_invoice')
            $address_id = (int) $this->id_address_invoice;
        elseif (count($product_list)) {
            $prod = current($product_list);
            $address_id = (int) $prod['id_address_delivery'];
        }
        else
            $address_id = null;
        if (!Address::addressExists($address_id))
            $address_id = null;

        $cache_id = 'getPackageShippingCost_' . (int) $this->id . '_' . (int) $address_id . '_' . (int) $id_carrier . '_' . (int) $use_tax . '_' . (int) $default_country->id;
        if ($products)
            foreach ($products as $product)
                $cache_id .= '_' . (int) $product['id_product'] . '_' . (int) $product['id_product_attribute'];

        if (Cache::isStored($cache_id))
            return Cache::retrieve($cache_id);

        // Order total in default currency without fees
        $order_total = $this->getOrderTotal(true, Cart::ONLY_PHYSICAL_PRODUCTS_WITHOUT_SHIPPING, $product_list);

        // Start with shipping cost at 0
        $shipping_cost = 0;
        // If no product added, return 0
        if (!count($products)) {
            Cache::store($cache_id, $shipping_cost);
            return $shipping_cost;
        }

        // Get id zone
        if (!$this->isMultiAddressDelivery()
                && isset($this->id_address_delivery) // Be carefull, id_address_delivery is not usefull one 1.5
                && $this->id_address_delivery
                && Customer::customerHasAddress($this->id_customer, $this->id_address_delivery
        ))
            $id_zone = Address::getZoneById((int) $this->id_address_delivery);
        else {
            if (!Validate::isLoadedObject($default_country))
                $default_country = new Country(Configuration::get('PS_COUNTRY_DEFAULT'), Configuration::get('PS_LANG_DEFAULT'));

            $id_zone = (int) $default_country->id_zone;
        }

        if ($id_carrier && !$this->isCarrierInRange((int) $id_carrier, (int) $id_zone))
            $id_carrier = '';

        if (empty($id_carrier) && $this->isCarrierInRange((int) Configuration::get('PS_CARRIER_DEFAULT'), (int) $id_zone))
            $id_carrier = (int) Configuration::get('PS_CARRIER_DEFAULT');

        if (empty($id_carrier)) {
            if ((int) $this->id_customer) {
                $customer = new Customer((int) $this->id_customer);
                $result = Carrier::getCarriers((int) Configuration::get('PS_LANG_DEFAULT'), true, false, (int) $id_zone, $customer->getGroups());
                unset($customer);
            }
            else
                $result = Carrier::getCarriers((int) Configuration::get('PS_LANG_DEFAULT'), true, false, (int) $id_zone);

            foreach ($result as $k => $row) {
                if ($row['id_carrier'] == Configuration::get('PS_CARRIER_DEFAULT'))
                    continue;

                if (!isset(self::$_carriers[$row['id_carrier']]))
                    self::$_carriers[$row['id_carrier']] = new Carrier((int) $row['id_carrier']);

                $carrier = self::$_carriers[$row['id_carrier']];

                // Get only carriers that are compliant with shipping method
                if (($carrier->getShippingMethod() == Carrier::SHIPPING_METHOD_WEIGHT && $carrier->getMaxDeliveryPriceByWeight((int) $id_zone) === false)
                        || ($carrier->getShippingMethod() == Carrier::SHIPPING_METHOD_PRICE && $carrier->getMaxDeliveryPriceByPrice((int) $id_zone) === false)) {
                    unset($result[$k]);
                    continue;
                }

                // If out-of-range behavior carrier is set on "Desactivate carrier"
                if ($row['range_behavior']) {
                    $check_delivery_price_by_weight = Carrier::checkDeliveryPriceByWeight($row['id_carrier'], $this->getTotalWeight(), (int) $id_zone);

                    $total_order = $this->getOrderTotal(true, Cart::BOTH_WITHOUT_SHIPPING, $product_list);
                    $check_delivery_price_by_price = Carrier::checkDeliveryPriceByPrice($row['id_carrier'], $total_order, (int) $id_zone, (int) $this->id_currency);

                    // Get only carriers that have a range compatible with cart
                    if (($carrier->getShippingMethod() == Carrier::SHIPPING_METHOD_WEIGHT && !$check_delivery_price_by_weight)
                            || ($carrier->getShippingMethod() == Carrier::SHIPPING_METHOD_PRICE && !$check_delivery_price_by_price)) {
                        unset($result[$k]);
                        continue;
                    }
                }

                if ($carrier->getShippingMethod() == Carrier::SHIPPING_METHOD_WEIGHT)
                    $shipping = $carrier->getDeliveryPriceByWeight($this->getTotalWeight($product_list), (int) $id_zone);
                else
                    $shipping = $carrier->getDeliveryPriceByPrice($order_total, (int) $id_zone, (int) $this->id_currency);

                if (!isset($min_shipping_price))
                    $min_shipping_price = $shipping;

                if ($shipping <= $min_shipping_price) {
                    $id_carrier = (int) $row['id_carrier'];
                    $min_shipping_price = $shipping;
                }
            }
        }

        if (empty($id_carrier))
            $id_carrier = Configuration::get('PS_CARRIER_DEFAULT');

        if (!isset(self::$_carriers[$id_carrier]))
            self::$_carriers[$id_carrier] = new Carrier((int) $id_carrier, Configuration::get('PS_LANG_DEFAULT'));

        $carrier = self::$_carriers[$id_carrier];

        if (!Validate::isLoadedObject($carrier))
            die(Tools::displayError('Fatal error: "no default carrier"'));

        if (!$carrier->active) {
            Cache::store($cache_id, $shipping_cost);
            return $shipping_cost;
        }

        // Free fees if free carrier
        if ($carrier->is_free == 1) {
            Cache::store($cache_id, 0);
            return 0;
        }

        // Select carrier tax
        if ($use_tax && !Tax::excludeTaxeOption()) {
            $address = Address::initialize((int) $address_id);
            $carrier_tax = $carrier->getTaxesRate($address);
        }

        $configuration = Configuration::getMultiple(array(
                    'PS_SHIPPING_FREE_PRICE',
                    'PS_SHIPPING_HANDLING',
                    'PS_SHIPPING_METHOD',
                    'PS_SHIPPING_FREE_WEIGHT'
                ));

        // Free fees
        $free_fees_price = 0;
        if (isset($configuration['PS_SHIPPING_FREE_PRICE']))
            $free_fees_price = Tools::convertPrice((float) $configuration['PS_SHIPPING_FREE_PRICE'], Currency::getCurrencyInstance((int) $this->id_currency));
        $orderTotalwithDiscounts = $this->getOrderTotal(true, Cart::BOTH_WITHOUT_SHIPPING, null, null, false);
        if ($orderTotalwithDiscounts >= (float) ($free_fees_price) && (float) ($free_fees_price) > 0) {
            Cache::store($cache_id, $shipping_cost);
            return $shipping_cost;
        }

        if (isset($configuration['PS_SHIPPING_FREE_WEIGHT'])
                && $this->getTotalWeight() >= (float) $configuration['PS_SHIPPING_FREE_WEIGHT']
                && (float) $configuration['PS_SHIPPING_FREE_WEIGHT'] > 0) {
            Cache::store($cache_id, $shipping_cost);
            return $shipping_cost;
        }

        // Get shipping cost using correct method
        if ($carrier->range_behavior) {
            // Get id zone
            if (isset($this->id_address_delivery)
                    && $this->id_address_delivery
                    && Customer::customerHasAddress($this->id_customer, $this->id_address_delivery))
                $id_zone = Address::getZoneById((int) $this->id_address_delivery);
            else
                $id_zone = (int) $default_country->id_zone;

            $check_delivery_price_by_weight = Carrier::checkDeliveryPriceByWeight((int) $carrier->id, $this->getTotalWeight(), (int) $id_zone);

            // Code Review V&V TO FINISH
            $check_delivery_price_by_price = Carrier::checkDeliveryPriceByPrice(
                            $carrier->id, $this->getOrderTotal(
                                    true, Cart::BOTH_WITHOUT_SHIPPING, $product_list
                            ), $id_zone, (int) $this->id_currency
            );

            if ((
                    $carrier->getShippingMethod() == Carrier::SHIPPING_METHOD_WEIGHT
                    && !$check_delivery_price_by_weight
                    ) || (
                    $carrier->getShippingMethod() == Carrier::SHIPPING_METHOD_PRICE
                    && !$check_delivery_price_by_price
                    ))
                $shipping_cost += 0;
            else {
                if ($carrier->getShippingMethod() == Carrier::SHIPPING_METHOD_WEIGHT)
                    $shipping_cost += $carrier->getDeliveryPriceByWeight($this->getTotalWeight($product_list), $id_zone);
                else // by price
                    $shipping_cost += $carrier->getDeliveryPriceByPrice($order_total, $id_zone, (int) $this->id_currency);
            }
        }
        else {
            if ($carrier->getShippingMethod() == Carrier::SHIPPING_METHOD_WEIGHT)
                $shipping_cost += $carrier->getDeliveryPriceByWeight($this->getTotalWeight($product_list), $id_zone);
            else
                $shipping_cost += $carrier->getDeliveryPriceByPrice($order_total, $id_zone, (int) $this->id_currency);
        }
        // Adding handling charges
        if (isset($configuration['PS_SHIPPING_HANDLING']) && $carrier->shipping_handling)
            $shipping_cost += (float) $configuration['PS_SHIPPING_HANDLING'];

        // Additional Shipping Cost per product
        foreach ($products as $product)
            if (!$product['is_virtual'])
                $shipping_cost += $product['additional_shipping_cost'] * $product['cart_quantity'];

        $shipping_cost = Tools::convertPrice($shipping_cost, Currency::getCurrencyInstance((int) $this->id_currency));

        //get external shipping cost from module
        if ($carrier->shipping_external) {
            $module_name = $carrier->external_module_name;
            $module = Module::getInstanceByName($module_name);

            if (Validate::isLoadedObject($module)) {
                if (array_key_exists('id_carrier', $module))
                    $module->id_carrier = $carrier->id;
                if ($carrier->need_range)
                    if (method_exists($module, 'getPackageShippingCost'))
                        $shipping_cost = $module->getPackageShippingCost($this, $shipping_cost, $products);
                    else
                        $shipping_cost = $module->getOrderShippingCost($this, $shipping_cost);
                else
                    $shipping_cost = $module->getOrderShippingCostExternal($this);

                // Check if carrier is available
                if ($shipping_cost === false) {
                    Cache::store($cache_id, false);
                    return false;
                }
            } else {
                Cache::store($cache_id, false);
                return false;
            }
        }

        // Apply tax
        if ($use_tax && isset($carrier_tax))
            $shipping_cost *= 1 + ($carrier_tax / 100);

        $shipping_cost = (float) Tools::ps_round((float) $shipping_cost, 2);
        Cache::store($cache_id, $shipping_cost);

        return $shipping_cost;
    }

    /**
     * Return cart weight
     * @return float Cart weight
     */
    public function getTotalWeight($products = null) {
        if (!is_null($products)) {
            $total_weight = 0;
            foreach ($products as $product) {
                if (!isset($product['weight_attribute']) || is_null($product['weight_attribute']))
                    $total_weight += $product['weight'] * $product['cart_quantity'];
                else
                    $total_weight += $product['weight_attribute'] * $product['cart_quantity'];
            }
            return $total_weight;
        }

        if (!isset(self::$_totalWeight[$this->id])) {
            if (Combination::isFeatureActive())
                $weight_product_with_attribute = Db::getInstance()->getValue('
				SELECT SUM((p.`weight` + pa.`weight`) * cp.`quantity`) as nb
				FROM `' . _DB_PREFIX_ . 'cart_product` cp
				LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (cp.`id_product` = p.`id_product`)
				LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON (cp.`id_product_attribute` = pa.`id_product_attribute`)
				WHERE (cp.`id_product_attribute` IS NOT NULL AND cp.`id_product_attribute` != 0)
				AND cp.`id_cart` = ' . (int) $this->id);
            else
                $weight_product_with_attribute = 0;

            $weight_product_without_attribute = Db::getInstance()->getValue('
			SELECT SUM(p.`weight` * cp.`quantity`) as nb
			FROM `' . _DB_PREFIX_ . 'cart_product` cp
			LEFT JOIN `' . _DB_PREFIX_ . 'product` p ON (cp.`id_product` = p.`id_product`)
			WHERE (cp.`id_product_attribute` IS NULL OR cp.`id_product_attribute` = 0)
			AND cp.`id_cart` = ' . (int) $this->id);

            self::$_totalWeight[$this->id] = round((float) $weight_product_with_attribute + (float) $weight_product_without_attribute, 3);
        }

        return self::$_totalWeight[$this->id];
    }

    /**
     * @deprecated 1.5.0
     */
    public function checkDiscountValidity($obj, $discounts, $order_total, $products, $check_cart_discount = false) {
        Tools::displayAsDeprecated();
        $context = Context::getContext()->cloneContext();
        $context->cart = $this;

        return $obj->checkValidity($context);
    }

    /**
     * Return useful informations for cart
     *
     * @return array Cart details
     */
    public function getSummaryDetails($id_lang = null, $refresh = false) {
        $context = Context::getContext();
        if (!$id_lang)
            $id_lang = $context->language->id;

        $delivery = new Address((int) $this->id_address_delivery);
        $invoice = new Address((int) $this->id_address_invoice);

        // New layout system with personalization fields
        $formatted_addresses['invoice'] = AddressFormat::getFormattedLayoutData($invoice);
        $formatted_addresses['delivery'] = AddressFormat::getFormattedLayoutData($delivery);

        $total_tax = $this->getOrderTotal() - $this->getOrderTotal(false);

        if ($total_tax < 0)
            $total_tax = 0;

        $currency = new Currency($this->id_currency);

        $products = $this->getProducts($refresh);
        $gift_products = array();
        $cart_rules = $this->getCartRules();
        $total_shipping = $this->getTotalShippingCost();
        $total_shipping_tax_exc = $this->getTotalShippingCost(null, false);
        $total_products_wt = $this->getOrderTotal(true, Cart::ONLY_PRODUCTS);
        $total_products = $this->getOrderTotal(false, Cart::ONLY_PRODUCTS);
        $total_discounts = $this->getOrderTotal(true, Cart::ONLY_DISCOUNTS);
        $total_discounts_tax_exc = $this->getOrderTotal(false, Cart::ONLY_DISCOUNTS);

        // The cart content is altered for display
        foreach ($cart_rules as &$cart_rule) {
            // If the cart rule is automatic (wihtout any code) and include free shipping, it should not be displayed as a cart rule but only set the shipping cost to 0
            if ($cart_rule['free_shipping'] && empty($cart_rule['code'])) {
                $cart_rule['value_real'] -= $total_shipping;
                $cart_rule['value_tax_exc'] = $total_shipping_tax_exc;

                // Update total shipping
                $total_shipping = 0;
                $total_shipping_tax_exc = 0;
            }
            if ($cart_rule['gift_product']) {
                foreach ($products as $key => &$product)
                    if (empty($product['gift']) && $product['id_product'] == $cart_rule['gift_product'] && $product['id_product_attribute'] == $cart_rule['gift_product_attribute']) {
                        // Update total products
                        $total_products_wt = Tools::ps_round($total_products_wt - $product['price_wt'], (int) $context->currency->decimals * _PS_PRICE_DISPLAY_PRECISION_);
                        $total_products = Tools::ps_round($total_products - $product['price'], (int) $context->currency->decimals * _PS_PRICE_DISPLAY_PRECISION_);

                        // Update total discounts
                        $total_discounts = Tools::ps_round($total_discounts - $product['price_wt'], (int) $context->currency->decimals * _PS_PRICE_DISPLAY_PRECISION_);
                        $total_discounts_tax_exc = Tools::ps_round($total_discounts_tax_exc - $product['price'], (int) $context->currency->decimals * _PS_PRICE_DISPLAY_PRECISION_);

                        // Update cart rule value
                        $cart_rule['value_real'] = Tools::ps_round($cart_rule['value_real'] - $product['price_wt'], (int) $context->currency->decimals * _PS_PRICE_DISPLAY_PRECISION_);
                        $cart_rule['value_tax_exc'] = Tools::ps_round($cart_rule['value_tax_exc'] - $product['price'], (int) $context->currency->decimals * _PS_PRICE_DISPLAY_PRECISION_);

                        // Update product quantity
                        $product['total_wt'] = Tools::ps_round($product['total_wt'] - $product['price_wt'], (int) $currency->decimals * _PS_PRICE_DISPLAY_PRECISION_);
                        $product['total'] = Tools::ps_round($product['total'] - $product['price'], (int) $currency->decimals * _PS_PRICE_DISPLAY_PRECISION_);
                        $product['cart_quantity']--;

                        if (!$product['cart_quantity'])
                            unset($products[$key]);

                        // Add a new product line
                        $gift_product = $product;
                        $gift_product['cart_quantity'] = 1;
                        $gift_product['price'] = 0;
                        $gift_product['price_wt'] = 0;
                        $gift_product['total_wt'] = 0;
                        $gift_product['total'] = 0;
                        $gift_product['gift'] = true;
                        $gift_products[] = $gift_product;

                        break; // One gift product per cart rule
                    }
            }
        }

        foreach ($cart_rules as $key => &$cart_rule)
            if ($cart_rule['value_real'] == 0)
                unset($cart_rules[$key]);

        return array(
            'delivery' => $delivery,
            'delivery_state' => State::getNameById($delivery->id_state),
            'invoice' => $invoice,
            'invoice_state' => State::getNameById($invoice->id_state),
            'formattedAddresses' => $formatted_addresses,
            'products' => array_values($products),
            'gift_products' => $gift_products,
            'discounts' => $cart_rules,
            'is_virtual_cart' => (int) $this->isVirtualCart(),
            'total_discounts' => $total_discounts,
            'total_discounts_tax_exc' => $total_discounts_tax_exc,
            'total_wrapping' => $this->getOrderTotal(true, Cart::ONLY_WRAPPING),
            'total_wrapping_tax_exc' => $this->getOrderTotal(false, Cart::ONLY_WRAPPING),
            'total_shipping' => $total_shipping,
            'total_shipping_tax_exc' => $total_shipping_tax_exc,
            'total_products_wt' => $total_products_wt,
            'total_products' => $total_products,
            'total_price' => $this->getOrderTotal(),
            'total_tax' => $total_tax,
            'total_price_without_tax' => $this->getOrderTotal(false),
            'is_multi_address_delivery' => $this->isMultiAddressDelivery() || ((int) Tools::getValue('multi-shipping') == 1),
            'free_ship' => $total_shipping ? 0 : 1,
            'carrier' => new Carrier($this->id_carrier, $id_lang),
        );
    }

    public function checkQuantities() {
        if (Configuration::get('PS_CATALOG_MODE'))
            return false;

        foreach ($this->getProducts() as $product)
            if (!$product['active']
                    || (
                    !$product['allow_oosp'] && $product['stock_quantity'] < $product['cart_quantity']
                    )
                    || !$product['available_for_order'])
                return false;

        return true;
    }

    public static function lastNoneOrderedCart($id_customer) {
        $sql = 'SELECT c.`id_cart`
				FROM ' . _DB_PREFIX_ . 'cart c
				WHERE c.`id_cart` NOT IN (SELECT o.`id_cart` FROM ' . _DB_PREFIX_ . 'orders o)
				AND c.`id_customer` = ' . (int) $id_customer . '
					' . Shop::addSqlRestriction(Shop::SHARE_ORDER, 'c') . '
				ORDER BY c.`date_upd` DESC';

        if (!$id_cart = Db::getInstance()->getValue($sql))
            return false;

        return $id_cart;
    }

    /**
     * Check if cart contains only virtual products
     *
     * @return boolean true if is a virtual cart or false
     */
    public function isVirtualCart($strict = false) {
        if (!ProductDownload::isFeatureActive())
            return false;

        if (!isset(self::$_isVirtualCart[$this->id])) {
            $products = $this->getProducts();
            if (!count($products))
                return false;

            $is_virtual = 1;
            foreach ($products as $product) {
                if (empty($product['is_virtual']))
                    $is_virtual = 0;
            }
            self::$_isVirtualCart[$this->id] = (int) $is_virtual;
        }

        return self::$_isVirtualCart[$this->id];
    }

    /**
     * Build cart object from provided id_order
     *
     * @param int $id_order
     * @return Cart|bool
     */
    public static function getCartByOrderId($id_order) {
        if ($id_cart = Cart::getCartIdByOrderId($id_order))
            return new Cart((int) $id_cart);

        return false;
    }

    public static function getCartIdByOrderId($id_order) {
        $result = Db::getInstance()->getRow('SELECT `id_cart` FROM ' . _DB_PREFIX_ . 'orders WHERE `id_order` = ' . (int) $id_order);
        if (!$result || empty($result) || !key_exists('id_cart', $result))
            return false;
        return $result['id_cart'];
    }

    /**
     * Add customer's text
     *
     * @params int $id_product
     * @params int $index
     * @params int $type
     * @params string $textValue
     *
     * @return bool Always true
     */
    public function addTextFieldToProduct($id_product, $index, $type, $text_value) {
        $text_value = str_replace(array("\n", "\r"), '', nl2br($text_value));
        $text_value = str_replace('\\', '\\\\', $text_value);
        $text_value = str_replace('\'', '\\\'', $text_value);
        return $this->_addCustomization($id_product, 0, $index, $type, $text_value, 0);
    }

    /**
     * Add customer's pictures
     *
     * @return bool Always true
     */
    public function addPictureToProduct($id_product, $index, $type, $file) {
        return $this->_addCustomization($id_product, 0, $index, $type, $file, 0);
    }

    public function deletePictureToProduct($id_product, $index) {
        Tools::displayAsDeprecated();
        return $this->deleteCustomizationToProduct($id_product, 0);
    }

    /**
     * Remove a customer's customization
     *
     * @param int $id_product
     * @param int $index
     * @return bool
     */
    public function deleteCustomizationToProduct($id_product, $index) {
        $result = true;

        $cust_data = Db::getInstance()->getRow('
			SELECT cu.`id_customization`, cd.`index`, cd.`value`, cd.`type` FROM `' . _DB_PREFIX_ . 'customization` cu
			LEFT JOIN `' . _DB_PREFIX_ . 'customized_data` cd
			ON cu.`id_customization` = cd.`id_customization`
			WHERE cu.`id_cart` = ' . (int) $this->id . '
			AND cu.`id_product` = ' . (int) $id_product . '
			AND `index` = ' . (int) $index . '
			AND `in_cart` = 0'
        );

        // Delete customization picture if necessary
        if ($cust_data['type'] == 0)
            $result &= (@unlink(_PS_UPLOAD_DIR_ . $cust_data['value']) && @unlink(_PS_UPLOAD_DIR_ . $cust_data['value'] . '_small'));

        $result &= Db::getInstance()->execute('DELETE
			FROM `' . _DB_PREFIX_ . 'customized_data`
			WHERE `id_customization` = ' . (int) $cust_data['id_customization'] . '
			AND `index` = ' . (int) $index
        );
        return $result;
    }

    /**
     * Return custom pictures in this cart for a specified product
     *
     * @param int $id_product
     * @param int $type only return customization of this type
     * @param bool $not_in_cart only return customizations that are not in cart already
     * @return array result rows
     */
    public function getProductCustomization($id_product, $type = null, $not_in_cart = false) {
        if (!Customization::isFeatureActive())
            return array();

        $result = Db::getInstance()->executeS('
			SELECT cu.id_customization, cd.index, cd.value, cd.type, cu.in_cart, cu.quantity
			FROM `' . _DB_PREFIX_ . 'customization` cu
			LEFT JOIN `' . _DB_PREFIX_ . 'customized_data` cd ON (cu.`id_customization` = cd.`id_customization`)
			WHERE cu.id_cart = ' . (int) $this->id . '
			AND cu.id_product = ' . (int) $id_product .
                ($type === Product::CUSTOMIZE_FILE ? ' AND type = ' . (int) Product::CUSTOMIZE_FILE : '') .
                ($type === Product::CUSTOMIZE_TEXTFIELD ? ' AND type = ' . (int) Product::CUSTOMIZE_TEXTFIELD : '') .
                ($not_in_cart ? ' AND in_cart = 0' : '')
        );
        return $result;
    }

    public static function getCustomerCarts($id_customer, $with_order = true) {
        return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
		SELECT *
		FROM ' . _DB_PREFIX_ . 'cart c
		WHERE c.`id_customer` = ' . (int) $id_customer . '
		' . (!$with_order ? 'AND id_cart NOT IN (SELECT id_cart FROM ' . _DB_PREFIX_ . 'orders o)' : '') . '
		ORDER BY c.`date_add` DESC');
    }

    public static function replaceZeroByShopName($echo, $tr) {
        return ($echo == '0' ? Configuration::get('PS_SHOP_NAME') : $echo);
    }

    public function duplicate() {
        if (!Validate::isLoadedObject($this))
            return false;

        $cart = new Cart($this->id);
        $cart->id = null;
        $cart->id_shop = $this->id_shop;
        $cart->id_shop_group = $this->id_shop_group;
        $cart->add();

        if (!Validate::isLoadedObject($cart))
            return false;

        $success = true;
        $products = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('SELECT * FROM `' . _DB_PREFIX_ . 'cart_product` WHERE `id_cart` = ' . (int) $this->id);

        foreach ($products as $product)
            $success &= $cart->updateQty(
                    $product['quantity'], (int) $product['id_product'], (int) $product['id_product_attribute'], null, 'up', (int) $product['id_address_delivery'], new Shop($cart->id_shop)
            );

        // Customized products
        $customs = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS('
			SELECT *
			FROM ' . _DB_PREFIX_ . 'customization c
			LEFT JOIN ' . _DB_PREFIX_ . 'customized_data cd ON cd.id_customization = c.id_customization
			WHERE c.id_cart = ' . (int) $this->id
        );

        // Get datas from customization table
        $customs_by_id = array();
        foreach ($customs as $custom) {
            if (!isset($customs_by_id[$custom['id_customization']]))
                $customs_by_id[$custom['id_customization']] = array(
                    'id_product_attribute' => $custom['id_product_attribute'],
                    'id_product' => $custom['id_product'],
                    'quantity' => $custom['quantity']
                );
        }

        // Insert new customizations
        $custom_ids = array();
        foreach ($customs_by_id as $customization_id => $val) {
            Db::getInstance()->execute('
				INSERT INTO `' . _DB_PREFIX_ . 'customization` (id_cart, id_product_attribute, id_product, `id_address_delivery`, quantity, `quantity_refunded`, `quantity_returned`, `in_cart`)
				VALUES(' . (int) $cart->id . ', ' . (int) $val['id_product_attribute'] . ', ' . (int) $val['id_product'] . ', ' . (int) $this->id_address_delivery . ', ' . (int) $val['quantity'] . ', 0, 0, 1)'
            );
            $custom_ids[$customization_id] = Db::getInstance(_PS_USE_SQL_SLAVE_)->Insert_ID();
        }

        // Insert customized_data
        if (count($customs)) {
            $first = true;
            $sql_custom_data = 'INSERT INTO ' . _DB_PREFIX_ . 'customized_data (`id_customization`, `type`, `index`, `value`) VALUES ';
            foreach ($customs as $custom) {
                if (!$first)
                    $sql_custom_data .= ',';
                else
                    $first = false;

                $sql_custom_data .= '(' . (int) $custom_ids[$custom['id_customization']] . ', ' . (int) $custom['type'] . ', ' .
                        (int) $custom['index'] . ', \'' . pSQL($custom['value']) . '\')';
            }
            Db::getInstance()->execute($sql_custom_data);
        }

        return array('cart' => $cart, 'success' => $success);
    }

    public function getWsCartRows() {
        $query = '
			SELECT id_product, id_product_attribute, quantity
			FROM `' . _DB_PREFIX_ . 'cart_product`
			WHERE id_cart = ' . (int) $this->id . '
				AND id_shop = ' . (int) Context::getContext()->shop->id;

        $result = Db::getInstance()->executeS($query);
        return $result;
    }

    public function setWsCartRows($values) {
        if ($this->deleteAssociations()) {
            $query = 'INSERT INTO `' . _DB_PREFIX_ . 'cart_product`(`id_cart`, `id_product`, `id_product_attribute`, `quantity`, `date_add`, `id_shop`) VALUES ';

            foreach ($values as $value)
                $query .= '(' . (int) $this->id . ', ' . (int) $value['id_product'] . ', ' .
                        (isset($value['id_product_attribute']) ? (int) $value['id_product_attribute'] : 'NULL') . ', ' . (int) $value['quantity'] . ', NOW(), ' . (int) Context::getContext()->shop->id . '),';

            Db::getInstance()->execute(rtrim($query, ','));
        }

        return true;
    }

    public function setProductAddressDelivery($id_product, $id_product_attribute, $old_id_address_delivery, $new_id_address_delivery) {
        // Check address is linked with the customer
        if (!Customer::customerHasAddress(Context::getContext()->customer->id, $new_id_address_delivery))
            return false;

        if ($new_id_address_delivery == $old_id_address_delivery)
            return false;

        // Checking if the product with the old address delivery exists
        $sql = new DbQuery();
        $sql->select('count(*)');
        $sql->from('cart_product', 'cp');
        $sql->where('id_product = ' . (int) $id_product);
        $sql->where('id_product_attribute = ' . (int) $id_product_attribute);
        $sql->where('id_address_delivery = ' . (int) $old_id_address_delivery);
        $sql->where('id_cart = ' . (int) $this->id);
        $result = Db::getInstance()->getValue($sql);

        if ($result == 0)
            return false;

        // Checking if there is no others similar products with this new address delivery
        $sql = new DbQuery();
        $sql->select('sum(quantity) as qty');
        $sql->from('cart_product', 'cp');
        $sql->where('id_product = ' . (int) $id_product);
        $sql->where('id_product_attribute = ' . (int) $id_product_attribute);
        $sql->where('id_address_delivery = ' . (int) $new_id_address_delivery);
        $sql->where('id_cart = ' . (int) $this->id);
        $result = Db::getInstance()->getValue($sql);

        // Removing similar products with this new address delivery
        $sql = 'DELETE FROM ' . _DB_PREFIX_ . 'cart_product
			WHERE id_product = ' . (int) $id_product . '
			AND id_product_attribute = ' . (int) $id_product_attribute . '
			AND id_address_delivery = ' . (int) $new_id_address_delivery . '
			AND id_cart = ' . (int) $this->id . '
			LIMIT 1';
        Db::getInstance()->execute($sql);

        // Changing the address
        $sql = 'UPDATE ' . _DB_PREFIX_ . 'cart_product
			SET `id_address_delivery` = ' . (int) $new_id_address_delivery . ',
			`quantity` = `quantity` + ' . (int) $result['sum'] . '
			WHERE id_product = ' . (int) $id_product . '
			AND id_product_attribute = ' . (int) $id_product_attribute . '
			AND id_address_delivery = ' . (int) $old_id_address_delivery . '
			AND id_cart = ' . (int) $this->id . '
			LIMIT 1';
        Db::getInstance()->execute($sql);

        // Changing the address of the customizations
        $sql = 'UPDATE ' . _DB_PREFIX_ . 'customization
			SET `id_address_delivery` = ' . (int) $new_id_address_delivery . '
			WHERE id_product = ' . (int) $id_product . '
			AND id_product_attribute = ' . (int) $id_product_attribute . '
			AND id_address_delivery = ' . (int) $old_id_address_delivery . '
			AND id_cart = ' . (int) $this->id;
        Db::getInstance()->execute($sql);

        return true;
    }

    public function duplicateProduct($id_product, $id_product_attribute, $id_address_delivery, $new_id_address_delivery, $quantity = 1, $keep_quantity = false) {
        // Check address is linked with the customer
        if (!Customer::customerHasAddress(Context::getContext()->customer->id, $new_id_address_delivery))
            return false;

        // Checking the product do not exist with the new address
        $sql = new DbQuery();
        $sql->select('count(*)');
        $sql->from('cart_product', 'c');
        $sql->where('id_product = ' . (int) $id_product);
        $sql->where('id_product_attribute = ' . (int) $id_product_attribute);
        $sql->where('id_address_delivery = ' . (int) $new_id_address_delivery);
        $sql->where('id_cart = ' . (int) $this->id);
        $result = Db::getInstance()->getValue($sql);

        if ($result > 0)
            return false;

        // Duplicating cart_product line
        $sql = 'INSERT INTO ' . _DB_PREFIX_ . 'cart_product
			(`id_cart`, `id_product`, `id_shop`, `id_product_attribute`, `quantity`, `date_add`, `id_address_delivery`)
			values(
				' . (int) $this->id . ',
				' . (int) $id_product . ',
				' . (int) $this->id_shop . ',
				' . (int) $id_product_attribute . ',
				' . (int) $quantity . ',
				NOW(),
				' . (int) $new_id_address_delivery . ')';

        Db::getInstance()->execute($sql);

        if (!$keep_quantity) {
            $sql = new DbQuery();
            $sql->select('quantity');
            $sql->from('cart_product', 'c');
            $sql->where('id_product = ' . (int) $id_product);
            $sql->where('id_product_attribute = ' . (int) $id_product_attribute);
            $sql->where('id_address_delivery = ' . (int) $id_address_delivery);
            $sql->where('id_cart = ' . (int) $this->id);
            $duplicatedQuantity = Db::getInstance()->getValue($sql);

            if ($duplicatedQuantity > $quantity) {
                $sql = 'UPDATE ' . _DB_PREFIX_ . 'cart_product
					SET `quantity` = `quantity` - ' . (int) $quantity . '
					WHERE id_cart = ' . (int) $this->id . '
					AND id_product = ' . (int) $id_product . '
					AND id_shop = ' . (int) $this->id_shop . '
					AND id_product_attribute = ' . (int) $id_product_attribute . '
					AND id_address_delivery = ' . (int) $id_address_delivery;
                Db::getInstance()->execute($sql);
            }
        }

        // Checking if there is customizations
        $sql = new DbQuery();
        $sql->select('*');
        $sql->from('customization', 'c');
        $sql->where('id_product = ' . (int) $id_product);
        $sql->where('id_product_attribute = ' . (int) $id_product_attribute);
        $sql->where('id_address_delivery = ' . (int) $id_address_delivery);
        $sql->where('id_cart = ' . (int) $this->id);
        $results = Db::getInstance()->executeS($sql);

        foreach ($results as $customization) {

            // Duplicate customization
            $sql = 'INSERT INTO ' . _DB_PREFIX_ . 'customization
				(`id_product_attribute`, `id_address_delivery`, `id_cart`, `id_product`, `quantity`, `in_cart`)
				VALUES (
					' . $customization['id_product_attribute'] . ',
					' . $new_id_address_delivery . ',
					' . $customization['id_cart'] . ',
					' . $customization['id_product'] . ',
					' . $quantity . ',
					' . $customization['in_cart'] . ')';
            Db::getInstance()->execute($sql);

            $sql = 'INSERT INTO ' . _DB_PREFIX_ . 'customized_data(`id_customization`, `type`, `index`, `value`)
				(
					SELECT ' . (int) Db::getInstance()->Insert_ID() . ' `id_customization`, `type`, `index`, `value`
					FROM customized_data
					WHERE id_customization = ' . $customization['id_customization'] . '
				)';
            Db::getInstance()->execute($sql);
        }

        $customization_count = count($results);
        if ($customization_count > 0) {
            $sql = 'UPDATE ' . _DB_PREFIX_ . 'cart_product
				SET `quantity` = `quantity` = ' . (int) $customization_count * $quantity . '
				WHERE id_cart = ' . (int) $this->id . '
				AND id_product = ' . (int) $id_product . '
				AND id_shop = ' . (int) $this->id_shop . '
				AND id_product_attribute = ' . (int) $id_product_attribute . '
				AND id_address_delivery = ' . (int) $new_id_address_delivery;
            Db::getInstance()->execute($sql);
        }

        return true;
    }

    /**
     * Update products cart address delivery with the address delivery of the cart
     */
    public function setNoMultishipping() {
        // Upgrading quantities
        $sql = 'SELECT sum(`quantity`) as quantity, id_product, id_product_attribute, count(*) as count
				FROM `' . _DB_PREFIX_ . 'cart_product`
				WHERE `id_cart` = ' . (int) $this->id . '
					AND `id_shop` = ' . (int) $this->id_shop . '
				GROUP BY id_product, id_product_attribute
				HAVING count > 1';

        foreach (Db::getInstance()->executeS($sql) as $product) {
            $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product`
				SET `quantity` = ' . $product['quantity'] . '
				WHERE  `id_cart` = ' . (int) $this->id . '
					AND `id_shop` = ' . (int) $this->id_shop . '
					AND id_product = ' . $product['id_product'] . '
					AND id_product_attribute = ' . $product['id_product_attribute'];
            Db::getInstance()->execute($sql);
        }

        // Merging multiple lines
        $sql = 'DELETE cp1
			FROM `' . _DB_PREFIX_ . 'cart_product` cp1
				INNER JOIN `' . _DB_PREFIX_ . 'cart_product` cp2
				ON (
					(cp1.id_cart = cp2.id_cart)
					AND (cp1.id_product = cp2.id_product)
					AND (cp1.id_product_attribute = cp2.id_product_attribute)
					AND (cp1.id_address_delivery <> cp2.id_address_delivery)
					AND (cp1.date_add > cp2.date_add)
				)';
        Db::getInstance()->execute($sql);

        // Upgrading address delivery
        $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product`
			SET `id_address_delivery` =
			(
				SELECT `id_address_delivery`
				FROM `' . _DB_PREFIX_ . 'cart`
				WHERE `id_cart` = ' . (int) $this->id . '
					AND `id_shop` = ' . (int) $this->id_shop . '
			)
			WHERE `id_cart` = ' . (int) $this->id . '
			' . (Configuration::get('PS_ALLOW_MULTISHIPPING') ? ' AND `id_shop` = ' . (int) $this->id_shop : '');

        Db::getInstance()->execute($sql);

        $sql = 'UPDATE `' . _DB_PREFIX_ . 'customization`
			SET `id_address_delivery` =
			(
				SELECT `id_address_delivery`
				FROM `' . _DB_PREFIX_ . 'cart`
				WHERE `id_cart` = ' . (int) $this->id . '
			)
			WHERE `id_cart` = ' . (int) $this->id;

        Db::getInstance()->execute($sql);
    }

    /**
     * Set an address to all products on the cart without address delivery
     */
    public function autosetProductAddress() {
        $id_address_delivery = 0;
        // Get the main address of the customer
        if ((int) $this->id_address_delivery > 0)
            $id_address_delivery = (int) $this->id_address_delivery;
        else
            $id_address_delivery = (int) Address::getFirstCustomerAddressId(Context::getContext()->customer->id);

        if (!$id_address_delivery)
            return;

        // Update
        $sql = 'UPDATE `' . _DB_PREFIX_ . 'cart_product`
			SET `id_address_delivery` = ' . (int) $id_address_delivery . '
			WHERE `id_cart` = ' . (int) $this->id . '
				AND (`id_address_delivery` = 0 OR `id_address_delivery` IS NULL)
				AND `id_shop` = ' . (int) $this->id_shop;
        Db::getInstance()->execute($sql);

        $sql = 'UPDATE `' . _DB_PREFIX_ . 'customization`
			SET `id_address_delivery` = ' . (int) $id_address_delivery . '
			WHERE `id_cart` = ' . (int) $this->id . '
				AND (`id_address_delivery` = 0 OR `id_address_delivery` IS NULL)';

        Db::getInstance()->execute($sql);
    }

    public function deleteAssociations() {
        return (Db::getInstance()->execute('
				DELETE FROM `' . _DB_PREFIX_ . 'cart_product`
				WHERE `id_cart` = ' . (int) $this->id) !== false);
    }

    /**
     * isGuestCartByCartId
     *
     * @param int $id_cart
     * @return bool true if cart has been made by a guest customer
     */
    public static function isGuestCartByCartId($id_cart) {
        if (!(int) $id_cart)
            return false;
        return (bool) Db::getInstance()->getValue('
			SELECT `is_guest`
			FROM `' . _DB_PREFIX_ . 'customer` cu
			LEFT JOIN `' . _DB_PREFIX_ . 'cart` ca ON (ca.`id_customer` = cu.`id_customer`)
			WHERE ca.`id_cart` = ' . (int) $id_cart);
    }

    /**
     * isCarrierInRange
     *
     * Check if the specified carrier is in range
     *
     * @id_carrier int
     * @id_zone int
     */
    public function isCarrierInRange($id_carrier, $id_zone) {
        $carrier = new Carrier((int) $id_carrier, Configuration::get('PS_LANG_DEFAULT'));
        $shipping_method = $carrier->getShippingMethod();
        if (!$carrier->range_behavior)
            return true;

        if ($shipping_method == Carrier::SHIPPING_METHOD_FREE)
            return true;

        $check_delivery_price_by_weight = Carrier::checkDeliveryPriceByWeight(
                        (int) $id_carrier, $this->getTotalWeight(), $id_zone
        );
        if ($shipping_method == Carrier::SHIPPING_METHOD_WEIGHT && $check_delivery_price_by_weight)
            return true;

        $check_delivery_price_by_price = Carrier::checkDeliveryPriceByPrice(
                        (int) $id_carrier, $this->getOrderTotal(
                                true, Cart::BOTH_WITHOUT_SHIPPING
                        ), $id_zone, (int) $this->id_currency
        );
        if ($shipping_method == Carrier::SHIPPING_METHOD_PRICE && $check_delivery_price_by_price)
            return true;

        return false;
    }

    /**
     * @param bool $ignore_virtual Ignore virtual product
     * @param bool $exclusive If true, the validation is exclusive : it must be present product in stock and out of stock
     * @since 1.5.0
     * 
     * @return bool false is some products from the cart are out of stock
     */
    public function isAllProductsInStock($ignore_virtual = false, $exclusive = false) {
        $product_out_of_stock = 0;
        $product_in_stock = 0;
        foreach ($this->getProducts() as $product) {
            if (!$exclusive) {
                if ((int) $product['quantity_available'] <= 0
                        && (!$ignore_virtual || !$product['is_virtual']))
                    return false;
            }
            else {
                if ((int) $product['quantity_available'] <= 0
                        && (!$ignore_virtual || !$product['is_virtual']))
                    $product_out_of_stock++;
                if ((int) $product['quantity_available'] > 0
                        && (!$ignore_virtual || !$product['is_virtual']))
                    $product_in_stock++;

                if ($product_in_stock > 0 && $product_out_of_stock > 0)
                    return false;
            }
        }
        return true;
    }

    /**
     * 
     * Execute hook displayCarrierList (extraCarrier) and merge theme to the $array
     * @param array $array
     */
    public static function addExtraCarriers(&$array) {
        $first = true;
        $hook_extracarrier_addr = array();
        foreach (Context::getContext()->cart->getAddressCollection() as $address) {
            $hook = Hook::exec('displayCarrierList', array('address' => $address));
            $hook_extracarrier_addr[$address->id] = $hook;

            if ($first) {
                $array = array_merge(
                        $array, array('HOOK_EXTRACARRIER' => $hook)
                );
                $first = false;
            }
            $array = array_merge(
                    $array, array('HOOK_EXTRACARRIER_ADDR' => $hook_extracarrier_addr)
            );
        }
    }

    /**
     * Get all the ids of the delivery addresses without carriers
     * 
     * @param bool $return_collection Return a collection
     * 
     * @return array Array of address id or of address object
     */
    public function getDeliveryAddressesWithoutCarriers($return_collection = false) {
        $addresses_without_carriers = array();
        foreach ($this->getProducts() as $product) {
            if (!in_array($product['id_address_delivery'], $addresses_without_carriers)
                    && !count(Carrier::getAvailableCarrierList(new Product($product['id_product']), null, $product['id_address_delivery'])))
                $addresses_without_carriers[] = $product['id_address_delivery'];
        }
        if (!$return_collection)
            return $addresses_without_carriers;
        else {
            $addresses_instance_without_carriers = array();
            foreach ($addresses_without_carriers as $id_address)
                $addresses_instance_without_carriers[] = new Address($id_address);
            return $addresses_instance_without_carriers;
        }
    }

    public function getProductsFromCartSerial($cart_serial) {
        $flag = Db::getInstance()->getValue("
			SELECT cart_id
			FROM `" . _DB_PREFIX_ . "cart_serial` where `serial` ='" . $cart_serial . "'");
        $oldCartId = $this->id;
//        if ($flag) {
//            $sql = 'SELECT id_customization,id_product_attribute,id_address_delivery,id_cart,id_product,quantity,quantity_refunded,quantity_returned,in_cart
//				FROM `' . _DB_PREFIX_ . 'customization`
//				WHERE `id_cart` = ' . (int)$flag;
//            foreach (Db::getInstance()->executeS($sql) as $customization) {
//                $old_id_customization=$customization['id_customization'];
//                $sql = 'INSERT INTO ' . _DB_PREFIX_ . 'customization(id_product_attribute,id_address_delivery,id_cart,id_product,quantity,quantity_refunded,quantity_returned,in_cart)
//			 values	('  . $customization['id_product_attribute'] . ',' . $customization['id_address_delivery'] . ',' .
//                        $this->id . ',' . $customization['id_product'] . ',' . $customization['quantity'] . ',' . $customization['quantity_refunded'] . ',' .
//                        $customization['quantity_returned'] . ',' . $customization['in_cart']
//                        . ')';
//                Db::getInstance()->execute($sql);
//                $new_id_customization= Db::getInstance()->Insert_ID();
//                
////                insert to customized_data
//                $sql='select  	`id_customization` ,`type`,`index`,`value` 
//                      FROM `' . _DB_PREFIX_ . 'customized_data`
//				 WHERE `id_customization` = ' . $old_id_customization;
//                foreach (Db::getInstance()->executeS($sql) as $customized_data) {
//                    $sql = 'INSERT INTO ' . _DB_PREFIX_ . 'customized_data(`id_customization` ,`type`,`index`,`value`)
//			 values	('  .$new_id_customization. ',' .$customized_data['type'] . ',' .
//                         $customized_data['index'] . ',' . $customized_data['value'] 
//                        . ')';
//                Db::getInstance()->execute($sql);
//                }
//            }
//        }
        $this->id = $flag;
        $listProducts = $this->getProducts(true);
        $this->id = $oldCartId;
        $arrayReturn = array(
            'old_cart' => $flag,
            'products' => $listProducts
        );
        return $arrayReturn;
    }

    public function addCartSerial() {
        $flag = Db::getInstance()->getValue('
			SELECT 1
			FROM `' . _DB_PREFIX_ . 'cart_serial` where `cart_id` =' . $this->id);
        if ($flag != '1') {
            Db::getInstance()->insert('cart_serial', array(
                'cart_id' => (int) $this->id,
                'serial' => substr(md5($this->id + 'lpv'), 0, 8),
                'created_date' => date('Y-m-d H:i:s')
            ));
        }
    }

    public function addCustomizationSerial($cart_id, $product_id, $ipa) {
        $sql = 'SELECT id_customization,id_product_attribute,id_address_delivery,id_cart,id_product,quantity,quantity_refunded,quantity_returned,in_cart
			FROM `' . _DB_PREFIX_ . 'customization`
				WHERE `id_cart` = ' . (int) $cart_id . ' and `id_product`=' . $product_id . ' and `id_product_attribute`=' . $ipa;
        foreach (Db::getInstance()->executeS($sql) as $customization) {
            $old_id_customization = $customization['id_customization'];
            $sql = 'INSERT INTO ' . _DB_PREFIX_ . 'customization(id_product_attribute,id_address_delivery,id_cart,id_product,quantity,quantity_refunded,quantity_returned,in_cart)
			 values	(' . $customization['id_product_attribute'] . ',' . $customization['id_address_delivery'] . ',' .
                    $this->id . ',' . $customization['id_product'] . ',0,' . $customization['quantity_refunded'] . ',' .
                    $customization['quantity_returned'] . ',' . $customization['in_cart']
                    . ')';
            Db::getInstance()->execute($sql);

            $new_id_customization = Db::getInstance()->Insert_ID();
//                insert to customized_data
            $sql = 'select  	`id_customization` ,`type`,`index`,`value` 
                      FROM `' . _DB_PREFIX_ . 'customized_data`
				 WHERE `id_customization` = ' . $old_id_customization;
            foreach (Db::getInstance()->executeS($sql) as $customized_data) {
                $sql = 'INSERT INTO ' . _DB_PREFIX_ . 'customized_data(`id_customization` ,`type`,`index`,`value`)
			 values	(' . $new_id_customization . ',' . $customized_data['type'] . ',' .
                        $customized_data['index'] . ',' . $customized_data['value']
                        . ')';
                Db::getInstance()->execute($sql);
            }
        }
        $varReturn = array('custom_id' => $new_id_customization);

        return $varReturn;
    }

}
